diff --git a/.ci/teamcity/bootstrap.sh b/.ci/teamcity/bootstrap.sh new file mode 100755 index 0000000000000..adb884ca064ba --- /dev/null +++ b/.ci/teamcity/bootstrap.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/util.sh" + +tc_start_block "Bootstrap" + +tc_start_block "yarn install and kbn bootstrap" +verify_no_git_changes yarn kbn bootstrap --prefer-offline +tc_end_block "yarn install and kbn bootstrap" + +tc_start_block "build kbn-pm" +verify_no_git_changes yarn kbn run build -i @kbn/pm +tc_end_block "build kbn-pm" + +tc_start_block "build plugin list docs" +verify_no_git_changes node scripts/build_plugin_list_docs +tc_end_block "build plugin list docs" + +tc_end_block "Bootstrap" diff --git a/.ci/teamcity/checks/bundle_limits.sh b/.ci/teamcity/checks/bundle_limits.sh new file mode 100755 index 0000000000000..3f7daef6d0473 --- /dev/null +++ b/.ci/teamcity/checks/bundle_limits.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +node scripts/build_kibana_platform_plugins --validate-limits diff --git a/.ci/teamcity/checks/doc_api_changes.sh b/.ci/teamcity/checks/doc_api_changes.sh new file mode 100755 index 0000000000000..821647a39441c --- /dev/null +++ b/.ci/teamcity/checks/doc_api_changes.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:checkDocApiChanges diff --git a/.ci/teamcity/checks/file_casing.sh b/.ci/teamcity/checks/file_casing.sh new file mode 100755 index 0000000000000..66578a4970fec --- /dev/null +++ b/.ci/teamcity/checks/file_casing.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:checkFileCasing diff --git a/.ci/teamcity/checks/i18n.sh b/.ci/teamcity/checks/i18n.sh new file mode 100755 index 0000000000000..f269816cf6b95 --- /dev/null +++ b/.ci/teamcity/checks/i18n.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:i18nCheck diff --git a/.ci/teamcity/checks/licenses.sh b/.ci/teamcity/checks/licenses.sh new file mode 100755 index 0000000000000..2baca87074630 --- /dev/null +++ b/.ci/teamcity/checks/licenses.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:licenses diff --git a/.ci/teamcity/checks/telemetry.sh b/.ci/teamcity/checks/telemetry.sh new file mode 100755 index 0000000000000..6413584d2057d --- /dev/null +++ b/.ci/teamcity/checks/telemetry.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:telemetryCheck diff --git a/.ci/teamcity/checks/test_hardening.sh b/.ci/teamcity/checks/test_hardening.sh new file mode 100755 index 0000000000000..21ee68e5ade70 --- /dev/null +++ b/.ci/teamcity/checks/test_hardening.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:test_hardening diff --git a/.ci/teamcity/checks/ts_projects.sh b/.ci/teamcity/checks/ts_projects.sh new file mode 100755 index 0000000000000..8afc195fee555 --- /dev/null +++ b/.ci/teamcity/checks/ts_projects.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:checkTsProjects diff --git a/.ci/teamcity/checks/type_check.sh b/.ci/teamcity/checks/type_check.sh new file mode 100755 index 0000000000000..da8ae3373d976 --- /dev/null +++ b/.ci/teamcity/checks/type_check.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:typeCheck diff --git a/.ci/teamcity/checks/verify_dependency_versions.sh b/.ci/teamcity/checks/verify_dependency_versions.sh new file mode 100755 index 0000000000000..4c2ddf5ce8612 --- /dev/null +++ b/.ci/teamcity/checks/verify_dependency_versions.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:verifyDependencyVersions diff --git a/.ci/teamcity/checks/verify_notice.sh b/.ci/teamcity/checks/verify_notice.sh new file mode 100755 index 0000000000000..8571e0bbceb13 --- /dev/null +++ b/.ci/teamcity/checks/verify_notice.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:verifyNotice diff --git a/.ci/teamcity/ci_stats.js b/.ci/teamcity/ci_stats.js new file mode 100644 index 0000000000000..2953661eca1fd --- /dev/null +++ b/.ci/teamcity/ci_stats.js @@ -0,0 +1,59 @@ +const https = require('https'); +const token = process.env.CI_STATS_TOKEN; +const host = process.env.CI_STATS_HOST; + +const request = (url, options, data = null) => { + const httpOptions = { + ...options, + headers: { + ...(options.headers || {}), + Authorization: `token ${token}`, + }, + }; + + return new Promise((resolve, reject) => { + console.log(`Calling https://${host}${url}`); + + const req = https.request(`https://${host}${url}`, httpOptions, (res) => { + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject(new Error(`Status Code: ${res.statusCode}`)); + } + + const data = []; + res.on('data', (d) => { + data.push(d); + }) + + res.on('end', () => { + try { + let resp = Buffer.concat(data).toString(); + + try { + if (resp.trim()) { + resp = JSON.parse(resp); + } + } catch (ex) { + console.error(ex); + } + + resolve(resp); + } catch (ex) { + reject(ex); + } + }); + }) + + req.on('error', reject); + + if (data) { + req.write(JSON.stringify(data)); + } + + req.end(); + }); +} + +module.exports = { + get: (url) => request(url, { method: 'GET' }), + post: (url, data) => request(url, { method: 'POST' }, data), +} diff --git a/.ci/teamcity/ci_stats_complete.js b/.ci/teamcity/ci_stats_complete.js new file mode 100644 index 0000000000000..0df9329167ff6 --- /dev/null +++ b/.ci/teamcity/ci_stats_complete.js @@ -0,0 +1,18 @@ +const ciStats = require('./ci_stats'); + +// This might be better as an API call in the future. +// Instead, it relies on a separate step setting the BUILD_STATUS env var. BUILD_STATUS is not something provided by TeamCity. +const BUILD_STATUS = process.env.BUILD_STATUS === 'SUCCESS' ? 'SUCCESS' : 'FAILURE'; + +(async () => { + try { + if (process.env.CI_STATS_BUILD_ID) { + await ciStats.post(`/v1/build/_complete?id=${process.env.CI_STATS_BUILD_ID}`, { + result: BUILD_STATUS, + }); + } + } catch (ex) { + console.error(ex); + process.exit(1); + } +})(); diff --git a/.ci/teamcity/default/accessibility.sh b/.ci/teamcity/default/accessibility.sh new file mode 100755 index 0000000000000..2868db9d067b8 --- /dev/null +++ b/.ci/teamcity/default/accessibility.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-accessibility +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-default" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "X-Pack accessibility tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/accessibility/config.ts diff --git a/.ci/teamcity/default/build.sh b/.ci/teamcity/default/build.sh new file mode 100755 index 0000000000000..af90e24ef5fe8 --- /dev/null +++ b/.ci/teamcity/default/build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +tc_start_block "Build Platform Plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ + --verbose +tc_end_block "Build Platform Plugins" + +export KBN_NP_PLUGINS_BUILT=true + +tc_start_block "Build Default Distribution" + +cd "$KIBANA_DIR" +node scripts/build --debug --no-oss +linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" +installDir="$KIBANA_DIR/install/kibana" +mkdir -p "$installDir" +tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + +tc_end_block "Build Default Distribution" diff --git a/.ci/teamcity/default/build_plugins.sh b/.ci/teamcity/default/build_plugins.sh new file mode 100755 index 0000000000000..76c553b4f8fa2 --- /dev/null +++ b/.ci/teamcity/default/build_plugins.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +tc_start_block "Build Platform Plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ + --verbose +tc_end_block "Build Platform Plugins" + +tc_set_env KBN_NP_PLUGINS_BUILT true diff --git a/.ci/teamcity/default/ci_group.sh b/.ci/teamcity/default/ci_group.sh new file mode 100755 index 0000000000000..26c2c563210ed --- /dev/null +++ b/.ci/teamcity/default/ci_group.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export CI_GROUP="$1" +export JOB=kibana-default-ciGroup${CI_GROUP} +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-default" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Default Distro Chrome Functional tests / Group ${CI_GROUP}" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "ciGroup$CI_GROUP" diff --git a/.ci/teamcity/default/firefox.sh b/.ci/teamcity/default/firefox.sh new file mode 100755 index 0000000000000..5922a72bd5e85 --- /dev/null +++ b/.ci/teamcity/default/firefox.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-firefoxSmoke +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-default" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "X-Pack firefox smoke test" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "includeFirefox" \ + --config test/functional/config.firefox.js \ + --config test/functional_embedded/config.firefox.ts diff --git a/.ci/teamcity/default/saved_object_field_metrics.sh b/.ci/teamcity/default/saved_object_field_metrics.sh new file mode 100755 index 0000000000000..f5b57ce3b06eb --- /dev/null +++ b/.ci/teamcity/default/saved_object_field_metrics.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-savedObjectFieldMetrics +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-default" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/saved_objects_field_count/config.ts diff --git a/.ci/teamcity/default/security_solution.sh b/.ci/teamcity/default/security_solution.sh new file mode 100755 index 0000000000000..46048f6c82d52 --- /dev/null +++ b/.ci/teamcity/default/security_solution.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-securitySolution +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-default" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Security Solution Cypress Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/security_solution_cypress/cli_config.ts diff --git a/.ci/teamcity/es_snapshots/build.sh b/.ci/teamcity/es_snapshots/build.sh new file mode 100755 index 0000000000000..f983713e80f4d --- /dev/null +++ b/.ci/teamcity/es_snapshots/build.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +cd .. +destination="$(pwd)/es-build" +mkdir -p "$destination" + +cd elasticsearch + +# These turn off automation in the Elasticsearch repo +export BUILD_NUMBER="" +export JENKINS_URL="" +export BUILD_URL="" +export JOB_NAME="" +export NODE_NAME="" + +# Reads the ES_BUILD_JAVA env var out of .ci/java-versions.properties and exports it +export "$(grep '^ES_BUILD_JAVA' .ci/java-versions.properties | xargs)" + +export PATH="$HOME/.java/$ES_BUILD_JAVA/bin:$PATH" +export JAVA_HOME="$HOME/.java/$ES_BUILD_JAVA" + +tc_start_block "Build Elasticsearch" +./gradlew -Dbuild.docker=true assemble --parallel +tc_end_block "Build Elasticsearch" + +tc_start_block "Create distribution archives" +find distribution -type f \( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \) -not -path '*no-jdk*' -not -path '*build-context*' -exec cp {} "$destination" \; +tc_end_block "Create distribution archives" + +ls -alh "$destination" + +tc_start_block "Create docker image archives" +docker images "docker.elastic.co/elasticsearch/elasticsearch" +docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 echo 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' +docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' +tc_end_block "Create docker image archives" + +cd "$destination" + +find ./* -exec bash -c "shasum -a 512 {} > {}.sha512" \; +ls -alh "$destination" diff --git a/.ci/teamcity/es_snapshots/create_manifest.js b/.ci/teamcity/es_snapshots/create_manifest.js new file mode 100644 index 0000000000000..63e54987f788f --- /dev/null +++ b/.ci/teamcity/es_snapshots/create_manifest.js @@ -0,0 +1,82 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); + +(async () => { + const destination = process.argv[2] || __dirname + '/test'; + + let ES_BRANCH = process.env.ELASTICSEARCH_BRANCH; + let GIT_COMMIT = process.env.ELASTICSEARCH_GIT_COMMIT; + let GIT_COMMIT_SHORT = execSync(`git rev-parse --short '${GIT_COMMIT}'`).toString().trim(); + + let VERSION = ''; + let SNAPSHOT_ID = ''; + let DESTINATION = ''; + + const now = new Date() + + // format: yyyyMMdd-HHmmss + const date = [ + now.getFullYear(), + (now.getMonth()+1).toString().padStart(2, '0'), + now.getDate().toString().padStart(2, '0'), + '-', + now.getHours().toString().padStart(2, '0'), + now.getMinutes().toString().padStart(2, '0'), + now.getSeconds().toString().padStart(2, '0'), + ].join('') + + try { + const files = fs.readdirSync(destination); + const manifestEntries = files + .filter(f => !f.match(/.sha512$/)) + .filter(f => !f.match(/.json$/)) + .map(filename => { + const 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-teamcity/${DESTINATION}/${filename}`, + version: parts[1], + platform: parts[3], + architecture: parts[4].split('.')[0], + license: parts[0] == 'oss' ? 'oss' : 'default', + } + }); + + const manifest = { + id: SNAPSHOT_ID, + bucket: `kibana-ci-es-snapshots-daily-teamcity/${DESTINATION}`.toString(), + branch: ES_BRANCH, + sha: GIT_COMMIT, + sha_short: GIT_COMMIT_SHORT, + version: VERSION, + generated: now.toISOString(), + archives: manifestEntries, + }; + + const manifestJSON = JSON.stringify(manifest, null, 2); + fs.writeFileSync(`${destination}/manifest.json`, manifestJSON); + + execSync(` + set -euo pipefail + cd "${destination}" + gsutil -m cp -r *.* gs://kibana-ci-es-snapshots-daily-teamcity/${DESTINATION} + cp manifest.json manifest-latest.json + gsutil cp manifest-latest.json gs://kibana-ci-es-snapshots-daily-teamcity/${VERSION} + `, { shell: '/bin/bash' }); + + console.log(`##teamcity[setParameter name='env.ES_SNAPSHOT_MANIFEST' value='https://storage.googleapis.com/kibana-ci-es-snapshots-daily-teamcity/${DESTINATION}/manifest.json']`); + console.log(`##teamcity[setParameter name='env.ES_SNAPSHOT_VERSION' value='${VERSION}']`); + console.log(`##teamcity[setParameter name='env.ES_SNAPSHOT_ID' value='${SNAPSHOT_ID}']`); + + console.log(`##teamcity[buildNumber '{build.number}-${VERSION}-${SNAPSHOT_ID}']`); + } catch (ex) { + console.error(ex); + process.exit(1); + } +})(); diff --git a/.ci/teamcity/es_snapshots/promote_manifest.js b/.ci/teamcity/es_snapshots/promote_manifest.js new file mode 100644 index 0000000000000..bcc79e696d783 --- /dev/null +++ b/.ci/teamcity/es_snapshots/promote_manifest.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); + +const BASE_BUCKET_DAILY = 'kibana-ci-es-snapshots-daily-teamcity'; +const BASE_BUCKET_PERMANENT = 'kibana-ci-es-snapshots-daily-teamcity/permanent'; + +(async () => { + try { + const MANIFEST_URL = process.argv[2]; + + if (!MANIFEST_URL) { + throw Error('Manifest URL missing'); + } + + if (!fs.existsSync('snapshot-promotion')) { + fs.mkdirSync('snapshot-promotion'); + } + process.chdir('snapshot-promotion'); + + execSync(`curl '${MANIFEST_URL}' > manifest.json`); + + const manifest = JSON.parse(fs.readFileSync('manifest.json')); + const { id, bucket, version } = manifest; + + console.log(`##teamcity[buildNumber '{build.number}-${version}-${id}']`); + + const manifestPermanent = { + ...manifest, + bucket: bucket.replace(BASE_BUCKET_DAILY, BASE_BUCKET_PERMANENT), + }; + + fs.writeFileSync('manifest-permanent.json', JSON.stringify(manifestPermanent, null, 2)); + + execSync( + ` + set -euo pipefail + + cp manifest.json manifest-latest-verified.json + gsutil cp manifest-latest-verified.json gs://${BASE_BUCKET_DAILY}/${version}/ + + rm manifest.json + cp manifest-permanent.json manifest.json + gsutil -m cp -r gs://${bucket}/* gs://${BASE_BUCKET_PERMANENT}/${version}/ + gsutil cp manifest.json gs://${BASE_BUCKET_PERMANENT}/${version}/ + + `, + { shell: '/bin/bash' } + ); + } catch (ex) { + console.error(ex); + process.exit(1); + } +})(); diff --git a/.ci/teamcity/oss/accessibility.sh b/.ci/teamcity/oss/accessibility.sh new file mode 100755 index 0000000000000..09693d7ebdc57 --- /dev/null +++ b/.ci/teamcity/oss/accessibility.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-accessibility +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-oss" + +checks-reporter-with-killswitch "Kibana accessibility tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/accessibility/config.ts diff --git a/.ci/teamcity/oss/build.sh b/.ci/teamcity/oss/build.sh new file mode 100755 index 0000000000000..3ef14b1663355 --- /dev/null +++ b/.ci/teamcity/oss/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +tc_start_block "Build Platform Plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --filter '!alertingExample' \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ + --verbose +tc_end_block "Build Platform Plugins" + +export KBN_NP_PLUGINS_BUILT=true + +tc_start_block "Build OSS Distribution" +node scripts/build --debug --oss + +# Renaming the build directory to a static one, so that we can put a static one in the TeamCity artifact rules +mv build/oss/kibana-*-SNAPSHOT-linux-x86_64 build/oss/kibana-build-oss +tc_end_block "Build OSS Distribution" diff --git a/.ci/teamcity/oss/build_plugins.sh b/.ci/teamcity/oss/build_plugins.sh new file mode 100755 index 0000000000000..28e3c9247f1d4 --- /dev/null +++ b/.ci/teamcity/oss/build_plugins.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +tc_start_block "Build Platform Plugins - OSS" + +node scripts/build_kibana_platform_plugins \ + --oss \ + --filter '!alertingExample' \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ + --verbose +tc_end_block "Build Platform Plugins - OSS" diff --git a/.ci/teamcity/oss/ci_group.sh b/.ci/teamcity/oss/ci_group.sh new file mode 100755 index 0000000000000..3b2fb7ea912b7 --- /dev/null +++ b/.ci/teamcity/oss/ci_group.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export CI_GROUP="$1" +export JOB="kibana-ciGroup$CI_GROUP" +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-oss" + +checks-reporter-with-killswitch "Functional tests / Group $CI_GROUP" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "ciGroup$CI_GROUP" diff --git a/.ci/teamcity/oss/firefox.sh b/.ci/teamcity/oss/firefox.sh new file mode 100755 index 0000000000000..5e2a6c17c0052 --- /dev/null +++ b/.ci/teamcity/oss/firefox.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-firefoxSmoke +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-oss" + +checks-reporter-with-killswitch "Firefox smoke test" \ + node scripts/functional_tests \ + --bail --debug \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "includeFirefox" \ + --config test/functional/config.firefox.js diff --git a/.ci/teamcity/oss/plugin_functional.sh b/.ci/teamcity/oss/plugin_functional.sh new file mode 100755 index 0000000000000..41ff549945c0b --- /dev/null +++ b/.ci/teamcity/oss/plugin_functional.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-pluginFunctional +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-oss" + +cd test/plugin_functional/plugins/kbn_sample_panel_action +if [[ ! -d "target" ]]; then + yarn build +fi +cd - + +yarn run grunt run:pluginFunctionalTestsRelease --from=source +yarn run grunt run:exampleFunctionalTestsRelease --from=source +yarn run grunt run:interpreterFunctionalTestsRelease diff --git a/.ci/teamcity/setup_ci_stats.js b/.ci/teamcity/setup_ci_stats.js new file mode 100644 index 0000000000000..6b381530d9bb7 --- /dev/null +++ b/.ci/teamcity/setup_ci_stats.js @@ -0,0 +1,33 @@ +const ciStats = require('./ci_stats'); + +(async () => { + try { + const build = await ciStats.post('/v1/build', { + jenkinsJobName: process.env.TEAMCITY_BUILDCONF_NAME, + jenkinsJobId: process.env.TEAMCITY_BUILD_ID, + jenkinsUrl: process.env.TEAMCITY_BUILD_URL, + prId: process.env.GITHUB_PR_NUMBER || null, + }); + + const config = { + apiUrl: `https://${process.env.CI_STATS_HOST}`, + apiToken: process.env.CI_STATS_TOKEN, + buildId: build.id, + }; + + const configJson = JSON.stringify(config); + process.env.KIBANA_CI_STATS_CONFIG = configJson; + console.log(`\n##teamcity[setParameter name='env.KIBANA_CI_STATS_CONFIG' display='hidden' password='true' value='${configJson}']\n`); + console.log(`\n##teamcity[setParameter name='env.CI_STATS_BUILD_ID' value='${build.id}']\n`); + + await ciStats.post(`/v1/git_info?buildId=${build.id}`, { + 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 + }); + } catch (ex) { + console.error(ex); + process.exit(1); + } +})(); diff --git a/.ci/teamcity/setup_env.sh b/.ci/teamcity/setup_env.sh new file mode 100755 index 0000000000000..f662d36247a2f --- /dev/null +++ b/.ci/teamcity/setup_env.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/util.sh" + +tc_set_env KIBANA_DIR "$(cd "$(dirname "$0")/../.." && pwd)" +tc_set_env XPACK_DIR "$KIBANA_DIR/x-pack" + +tc_set_env CACHE_DIR "$HOME/.kibana" +tc_set_env PARENT_DIR "$(cd "$KIBANA_DIR/.."; pwd)" +tc_set_env WORKSPACE "${WORKSPACE:-$PARENT_DIR}" + +tc_set_env KIBANA_PKG_BRANCH "$(jq -r .branch "$KIBANA_DIR/package.json")" +tc_set_env KIBANA_BASE_BRANCH "$KIBANA_PKG_BRANCH" + +tc_set_env GECKODRIVER_CDNURL "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" +tc_set_env CHROMEDRIVER_CDNURL "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" +tc_set_env RE2_DOWNLOAD_MIRROR "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" +tc_set_env CYPRESS_DOWNLOAD_MIRROR "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" + +tc_set_env NODE_OPTIONS "${NODE_OPTIONS:-} --max-old-space-size=4096" + +tc_set_env FORCE_COLOR 1 +tc_set_env TEST_BROWSER_HEADLESS 1 + +tc_set_env ELASTIC_APM_ENVIRONMENT ci + +if [[ "${KIBANA_CI_REPORTER_KEY_BASE64-}" ]]; then + tc_set_env KIBANA_CI_REPORTER_KEY "$(echo "$KIBANA_CI_REPORTER_KEY_BASE64" | base64 -d)" +fi + +if is_pr; then + tc_set_env CHECKS_REPORTER_ACTIVE true + + # 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" +else + tc_set_env CHECKS_REPORTER_ACTIVE false +fi + +tc_set_env FLEET_PACKAGE_REGISTRY_PORT 6104 # Any unused port is fine, used by ingest manager tests + +if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then + echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" + tc_set_env DETECT_CHROMEDRIVER_VERSION true + tc_set_env CHROMEDRIVER_FORCE_DOWNLOAD true +else + echo "Chrome not detected, installing default chromedriver binary for the package version" +fi diff --git a/.ci/teamcity/setup_node.sh b/.ci/teamcity/setup_node.sh new file mode 100755 index 0000000000000..b805a2aa6fe62 --- /dev/null +++ b/.ci/teamcity/setup_node.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/util.sh" + +tc_start_block "Setup Node" + +tc_set_env NODE_VERSION "$(cat "$KIBANA_DIR/.node-version")" +tc_set_env NODE_DIR "$CACHE_DIR/node/$NODE_VERSION" +tc_set_env NODE_BIN_DIR "$NODE_DIR/bin" +tc_set_env YARN_OFFLINE_CACHE "$CACHE_DIR/yarn-offline-cache" + +if [[ ! -d "$NODE_DIR" ]]; then + nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" + + echo "node.js v$NODE_VERSION not found at $NODE_DIR, downloading from $nodeUrl" + + mkdir -p "$NODE_DIR" + curl --silent -L "$nodeUrl" | tar -xz -C "$NODE_DIR" --strip-components=1 +else + echo "node.js v$NODE_VERSION already installed to $NODE_DIR, re-using" + ls -alh "$NODE_BIN_DIR" +fi + +tc_set_env PATH "$NODE_BIN_DIR:$PATH" + +tc_end_block "Setup Node" +tc_start_block "Setup Yarn" + +tc_set_env YARN_VERSION "$(node -e "console.log(String(require('./package.json').engines.yarn || '').replace(/^[^\d]+/,''))")" + +if [[ ! $(which yarn) || $(yarn --version) != "$YARN_VERSION" ]]; then + npm install -g "yarn@^${YARN_VERSION}" +fi + +yarn config set yarn-offline-mirror "$YARN_OFFLINE_CACHE" + +tc_set_env YARN_GLOBAL_BIN "$(yarn global bin)" +tc_set_env PATH "$PATH:$YARN_GLOBAL_BIN" + +tc_end_block "Setup Yarn" diff --git a/.ci/teamcity/tests/mocha.sh b/.ci/teamcity/tests/mocha.sh new file mode 100755 index 0000000000000..ea6c43c39e397 --- /dev/null +++ b/.ci/teamcity/tests/mocha.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:mocha diff --git a/.ci/teamcity/tests/test_hardening.sh b/.ci/teamcity/tests/test_hardening.sh new file mode 100755 index 0000000000000..21ee68e5ade70 --- /dev/null +++ b/.ci/teamcity/tests/test_hardening.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:test_hardening diff --git a/.ci/teamcity/tests/test_projects.sh b/.ci/teamcity/tests/test_projects.sh new file mode 100755 index 0000000000000..3feaa821424e1 --- /dev/null +++ b/.ci/teamcity/tests/test_projects.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +yarn run grunt run:test_projects diff --git a/.ci/teamcity/tests/xpack_list_cyclic_dependency.sh b/.ci/teamcity/tests/xpack_list_cyclic_dependency.sh new file mode 100755 index 0000000000000..39f79f94744c7 --- /dev/null +++ b/.ci/teamcity/tests/xpack_list_cyclic_dependency.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +cd x-pack +checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/.ci/teamcity/tests/xpack_siem_cyclic_dependency.sh b/.ci/teamcity/tests/xpack_siem_cyclic_dependency.sh new file mode 100755 index 0000000000000..e3829c961fac8 --- /dev/null +++ b/.ci/teamcity/tests/xpack_siem_cyclic_dependency.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +cd x-pack +checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/.ci/teamcity/util.sh b/.ci/teamcity/util.sh new file mode 100755 index 0000000000000..fe1afdf04c54c --- /dev/null +++ b/.ci/teamcity/util.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +tc_escape() { + escaped="$1" + + # See https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values + + escaped="$(echo "$escaped" | sed -z 's/|/||/g')" + escaped="$(echo "$escaped" | sed -z "s/'/|'/g")" + escaped="$(echo "$escaped" | sed -z 's/\[/|\[/g')" + escaped="$(echo "$escaped" | sed -z 's/\]/|\]/g')" + escaped="$(echo "$escaped" | sed -z 's/\n/|n/g')" + escaped="$(echo "$escaped" | sed -z 's/\r/|r/g')" + + echo "$escaped" +} + +# Sets up an environment variable locally, and also makes it available for subsequent steps in the build +# NOTE: env vars set up this way will be visible in the UI when logged in unless you set them up as blank password parameters ahead of time. +tc_set_env() { + export "$1"="$2" + echo "##teamcity[setParameter name='env.$1' value='$(tc_escape "$2")']" +} + +verify_no_git_changes() { + RED='\033[0;31m' + C_RESET='\033[0m' # Reset color + + "$@" + + GIT_CHANGES="$(git ls-files --modified)" + if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: '$*' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 + fi +} + +tc_start_block() { + echo "##teamcity[blockOpened name='$1']" +} + +tc_end_block() { + echo "##teamcity[blockClosed name='$1']" +} + +checks-reporter-with-killswitch() { + if [ "$CHECKS_REPORTER_ACTIVE" == "true" ] ; then + yarn run github-checks-reporter "$@" + else + arguments=("$@"); + "${arguments[@]:1}"; + fi +} + +is_pr() { + [[ "${GITHUB_PR_NUMBER-}" ]] && return + false +} + +# This function is specifcally for retrying test runner steps one time +# A different solution should be used for retrying general steps (e.g. bootstrap) +tc_retry() { + tc_start_block "Retryable Step - Attempt #1" + "$@" || { + tc_end_block "Retryable Step - Attempt #1" + tc_start_block "Retryable Step - Attempt #2" + >&2 echo "First attempt failed. Retrying $*" + if "$@"; then + echo 'Second attempt successful' + echo "##teamcity[buildStatus status='SUCCESS' text='{build.status.text} with a flaky failure']" + echo "##teamcity[setParameter name='elastic.build.flaky' value='true']" + tc_end_block "Retryable Step - Attempt #2" + else + status="$?" + tc_end_block "Retryable Step - Attempt #2" + return "$status" + fi + } + tc_end_block "Retryable Step - Attempt #1" +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5b43f9883a2c1..93d49dc18d417 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -162,6 +162,8 @@ /src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations +/.ci/teamcity/ @elastic/kibana-operations +/.teamcity/ @elastic/kibana-operations /vars/ @elastic/kibana-operations #CC# /packages/kbn-expect/ @elastic/kibana-operations diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 96284345d1631..d9d2d6d1ddb8b 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppArch", "projectNumber": 37, "columnName": "To triage"}, {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/.teamcity/.editorconfig b/.teamcity/.editorconfig new file mode 100644 index 0000000000000..db789a8c72de1 --- /dev/null +++ b/.teamcity/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +disabled_rules=no-wildcard-imports +indent_size=2 +kotlin_imports_layout=idea diff --git a/.teamcity/Kibana.png b/.teamcity/Kibana.png new file mode 100644 index 0000000000000..c8f78f4575965 Binary files /dev/null and b/.teamcity/Kibana.png differ diff --git a/.teamcity/README.md b/.teamcity/README.md new file mode 100644 index 0000000000000..77c0bc5bc4cd3 --- /dev/null +++ b/.teamcity/README.md @@ -0,0 +1,156 @@ +# Kibana TeamCity + +## Implemented so far + +- Project configuration with ability to provide configuration values that are unique per TeamCity instance (e.g. dev vs prod) +- Read-only configuration (no editing through the UI) +- Secrets stored in TeamCity outside of source control +- Setting secret environment variables (they get filtered from console if output on accident) +- GCP agent configurations + - One-time use agents + - Multiple agents configured, of different sizes (cpu, memory) + - Require specific agents per build configuration +- Unit testable DSL code +- Build artifact generation and consumption +- DSL Extensions of various kinds to easily share common configuration between build configurations in the same repo +- Barebones Slack notifications via plugin +- Dynamically creating environment variables / secrets at runtime for subsequent steps +- "Baseline CI" job that runs a subset of CI for every commit +- "Hourly CI" job that runs full CI hourly, if changes are detected. Re-uses builds that ran during "Baseline CI" for same commit +- Performance monitoring enabled for all jobs +- Jobs with multiple VCS roots (Kibana + Elasticsearch) +- GCS uploading using service account key file and gsutil +- Job that has a version string as an "output", rather than an artifact/file, with consumption in a different job +- Clone a list of jobs and modify dependencies/configuration for a second pipeline +- Promote/deploy a built artifact through the UI by selecting previously built artifact (or automatically build a new one and deploy if successful) +- Custom Build IDs using service messages + +## Pull Requests + +The `Pull Request` feature in TeamCity: + +- Automatically discovers pull request branches in GitHub + - Option to filter by contributor type (members of same org, org+external contributor, everyone) + - Option to filter by target branch (e.g. only discover Pull Requests targeting master) + - Works by essentially modifying the VCS root branch spec (so you should NOT add anything related to PRs to branch spec if you are using this) + - Draft PRs do get discovered +- Adds some Pull Request information to build overview pages +- Adds a few parameters available to build configurations: + - teamcity.pullRequest.number + - teamcity.pullRequest.title + - teamcity.pullRequest.source.branch + - teamcity.pullRequest.target.branch + - (Notice that source owner is not available - there's no information for forks) +- Requires a token for API interaction + +That's it. There's no interaction with labels/comments/etc. Triggering is handled via the standard triggering options. + +So, if you only want to: + +- Build on new commit (e.g. not via comment) or via the TeamCity UI +- Start builds for users not covered by the filter options using the TeamCity UI + +The Pull Request feature may be enough to cover your needs. Otherwise, you'll need something additional (an external bot, or a new teamcity plugin, etc). + +### Other PR notes + +- TeamCity doesn't have the ability to cancel currently-running builds when a new commit is pushed +- TeamCity does not add fork information (e.g. the owner) to build configuration parameters +- Builds CAN be triggered for branches not yet discovered + - You can turn off discovery altogether, and a branch will still be build-able. When triggered externally, it will show up in the UI and build. + +How to [trigger a build via API](https://www.jetbrains.com/help/teamcity/rest-api-reference.html#Triggering+a+Build): + +``` +POST https://teamcity-server/app/rest/buildQueue + + + + +``` + +and with additional properties: + +``` + + + + + + + +``` + +## Kibana Builds + +### Baseline CI + +- Generates baseline metrics needed for PR comparisons +- Only runs OSS and default builds, and generates default saved object field metrics +- Runs for each commit (each build should build a single commit) + +### Full CI + +- Runs everything in CI - all tests and builds +- Re-uses builds from Baseline CI if they are finished or in-progress +- Not generally triggered directly, is triggered by other jobs + +### Hourly CI + +- Triggers every hour and groups up all changes since the last run +- Runs whatever is in `Full CI` + +### Pull Request CI + +- Kibana TeamCity PR bot triggers this build for PRs (new commits, trigger comments) +- Sets many PR related parameters/env vars, then runs `Full CI` + +![Diagram](Kibana.png) + +### ES Snapshot Verification + +Build Configurations: + +- Build Snapshot +- Test Builds (e.g. OSS CI Group 1, Default CI Group 3, etc) +- Verify Snapshot +- Promote Snapshot +- Immediately Promote Snapshot + +Desires: + +- Build ES snapshot on a daily basis, run E2E tests against it, promote when successful +- Ability to easily promote old builds that have been verified +- Ability to run verification without promoting it + +#### Build Snapshot + +- checks out both Kibana and ES codebases +- builds ES artifacts +- uses scripts from Kibana repo to create JSON manifest and assemble snapshot files +- uploads artifacts to GCS +- sets parameters via service message that contains the snapshot URL, ID, version so they can be consumed by downstream jobs +- triggers on timer, once per day + +#### Test Builds + +- builds are clones of all "essential ci" functional and integration tests with irrelevant features disabled + - they are clones because runs of this build and runs of the essential ci versions for the same commit hash mean different things +- snapshot dependency on `Build Elasticsearch Snapshot` is added to clones +- set `env.ES_SNAPSHOT_MANIFEST` = `dep..ES_SNAPSHOT_MANIFEST` to "consume" the built artifact + +#### Verify Snapshot + +- composite build that contains all of the cloned test builds + +#### Promote Snapshot + +- snapshot dependency on `Build Snapshot` and `Verify Snapshot` +- uses scripts from Kibana repo to promote elasticsearch snapshot from `Build Snapshot` by updating manifest files in GCS +- triggers whenever a build of `Verify Snapshot` completes successfully + +#### Immediately Promote Snapshot + +- snapshot dependency only on `Build Snapshot` +- same as `Promote Snapshot` but skips testing +- can only be triggered manually diff --git a/.teamcity/pom.xml b/.teamcity/pom.xml new file mode 100644 index 0000000000000..5fa068d0a92e0 --- /dev/null +++ b/.teamcity/pom.xml @@ -0,0 +1,128 @@ + + + + + 4.0.0 + Kibana Teamcity Config DSL Script + org.elastic.kibana + kibana-teamcity-dsl + 1.0-SNAPSHOT + + + org.jetbrains.teamcity + configs-dsl-kotlin-parent + 1.0-SNAPSHOT + + + + + jetbrains-all + https://download.jetbrains.com/teamcity-repository + + true + + + + teamcity-server + https://ci.elastic.dev/app/dsl-plugins-repository + + true + + + + + + + JetBrains + https://download.jetbrains.com/teamcity-repository + + + + + tests + src + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + + compile + process-sources + + compile + + + + test-compile + process-test-sources + + test-compile + + + + + + org.jetbrains.teamcity + teamcity-configs-maven-plugin + ${teamcity.dsl.version} + + kotlin + target/generated-configs + + + + + + + + org.jetbrains.teamcity + configs-dsl-kotlin + ${teamcity.dsl.version} + compile + + + org.jetbrains.teamcity + configs-dsl-kotlin-plugins + 1.0-SNAPSHOT + pom + compile + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + compile + + + org.jetbrains.kotlin + kotlin-script-runtime + ${kotlin.version} + compile + + + junit + junit + 4.13 + + + diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts new file mode 100644 index 0000000000000..ec1b1c6eb94ef --- /dev/null +++ b/.teamcity/settings.kts @@ -0,0 +1,12 @@ +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import projects.Kibana +import projects.KibanaConfiguration + +version = "2020.1" + +val config = KibanaConfiguration { + agentNetwork = DslContext.getParameter("agentNetwork", "teamcity") + agentSubnet = DslContext.getParameter("agentSubnet", "teamcity") +} + +project(Kibana(config)) diff --git a/.teamcity/src/Extensions.kt b/.teamcity/src/Extensions.kt new file mode 100644 index 0000000000000..120b333d43e72 --- /dev/null +++ b/.teamcity/src/Extensions.kt @@ -0,0 +1,169 @@ +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.notifications +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.ScriptBuildStep +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import jetbrains.buildServer.configs.kotlin.v2019_2.ui.insert +import projects.kibanaConfiguration + +fun BuildFeatures.junit(dirs: String = "target/**/TEST-*.xml") { + feature { + type = "xml-report-plugin" + param("xmlReportParsing.reportType", "junit") + param("xmlReportParsing.reportDirs", dirs) + } +} + +fun ProjectFeatures.kibanaAgent(init: ProjectFeature.() -> Unit) { + feature { + type = "CloudImage" + param("network", kibanaConfiguration.agentNetwork) + param("subnet", kibanaConfiguration.agentSubnet) + param("growingId", "true") + param("agent_pool_id", "-2") + param("preemptible", "false") + param("sourceProject", "elastic-images-prod") + param("sourceImageFamily", "elastic-kibana-ci-ubuntu-1804-lts") + param("zone", "us-central1-a") + param("profileId", "kibana") + param("diskType", "pd-ssd") + param("machineCustom", "false") + param("maxInstances", "200") + param("imageType", "ImageFamily") + param("diskSizeGb", "75") // TODO + init() + } +} + +fun ProjectFeatures.kibanaAgent(size: String, init: ProjectFeature.() -> Unit = {}) { + kibanaAgent { + id = "KIBANA_STANDARD_$size" + param("source-id", "kibana-standard-$size-") + param("machineType", "n2-standard-$size") + init() + } +} + +fun BuildType.kibanaAgent(size: String) { + requirements { + startsWith("teamcity.agent.name", "kibana-standard-$size-", "RQ_AGENT_NAME") + } +} + +fun BuildType.kibanaAgent(size: Int) { + kibanaAgent(size.toString()) +} + +val testArtifactRules = """ + target/kibana-* + target/test-metrics/* + target/kibana-security-solution/**/*.png + target/junit/**/* + target/test-suites-ci-plan.json + test/**/screenshots/session/*.png + test/**/screenshots/failure/*.png + test/**/screenshots/diff/*.png + test/functional/failure_debug/html/*.html + x-pack/test/**/screenshots/session/*.png + x-pack/test/**/screenshots/failure/*.png + x-pack/test/**/screenshots/diff/*.png + x-pack/test/functional/failure_debug/html/*.html + x-pack/test/functional/apps/reporting/reports/session/*.pdf + """.trimIndent() + +fun BuildType.addTestSettings() { + artifactRules += "\n" + testArtifactRules + steps { + failedTestReporter() + } + features { + junit() + } +} + +fun BuildType.addSlackNotifications(to: String = "#kibana-teamcity-testing") { + params { + param("elastic.slack.enabled", "true") + param("elastic.slack.channels", to) + } +} + +fun BuildType.dependsOn(buildType: BuildType, init: SnapshotDependency.() -> Unit = {}) { + dependencies { + snapshot(buildType) { + reuseBuilds = ReuseBuilds.SUCCESSFUL + onDependencyCancel = FailureAction.ADD_PROBLEM + onDependencyFailure = FailureAction.ADD_PROBLEM + synchronizeRevisions = true + init() + } + } +} + +fun BuildType.dependsOn(vararg buildTypes: BuildType, init: SnapshotDependency.() -> Unit = {}) { + buildTypes.forEach { dependsOn(it, init) } +} + +fun BuildSteps.failedTestReporter(init: ScriptBuildStep.() -> Unit = {}) { + script { + name = "Failed Test Reporter" + scriptContent = + """ + #!/bin/bash + node scripts/report_failed_tests + """.trimIndent() + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + init() + } +} + +// Note: This is currently only used for tests and has a retry in it for flaky tests. +// The retry should be refactored if runbld is ever needed for other tasks. +fun BuildSteps.runbld(stepName: String, script: String) { + script { + name = stepName + + // The indentation for this string is like this to ensure 100% that the RUNBLD-SCRIPT heredoc termination will not have spaces at the beginning + scriptContent = +"""#!/bin/bash + +set -euo pipefail + +source .ci/teamcity/util.sh + +branchName="${'$'}GIT_BRANCH" +branchName="${'$'}{branchName#refs\/heads\/}" + +if [[ "${'$'}{GITHUB_PR_NUMBER-}" ]]; then + branchName=pull-request +fi + +project=kibana +if [[ "${'$'}{ES_SNAPSHOT_MANIFEST-}" ]]; then + project=kibana-es-snapshot-verify +fi + +# These parameters are only for runbld reporting +export JENKINS_HOME="${'$'}HOME" +export BUILD_URL="%teamcity.serverUrl%/build/%teamcity.build.id%" +export branch_specifier=${'$'}branchName +export NODE_LABELS='teamcity' +export BUILD_NUMBER="%build.number%" +export EXECUTOR_NUMBER='' +export NODE_NAME='' + +export OLD_PATH="${'$'}PATH" + +file=${'$'}(mktemp) + +( +cat < ${'$'}file + +tc_retry /usr/local/bin/runbld -d "${'$'}(pwd)" --job-name="elastic+${'$'}project+${'$'}branchName" ${'$'}file +""" + } +} diff --git a/.teamcity/src/builds/BaselineCi.kt b/.teamcity/src/builds/BaselineCi.kt new file mode 100644 index 0000000000000..ae316960acf89 --- /dev/null +++ b/.teamcity/src/builds/BaselineCi.kt @@ -0,0 +1,38 @@ +package builds + +import addSlackNotifications +import builds.default.DefaultBuild +import builds.default.DefaultSavedObjectFieldMetrics +import builds.oss.OssBuild +import dependsOn +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 +import templates.KibanaTemplate + +object BaselineCi : BuildType({ + id("Baseline_CI") + name = "Baseline CI" + description = "Runs builds, saved object field metrics for every commit" + type = Type.COMPOSITE + paused = true + + templates(KibanaTemplate) + + triggers { + vcs { + branchFilter = "refs/heads/master_teamcity" +// perCheckinTriggering = true // TODO re-enable this later, it wreaks havoc when I merge upstream + } + } + + dependsOn( + OssBuild, + DefaultBuild, + DefaultSavedObjectFieldMetrics + ) { + onDependencyCancel = FailureAction.ADD_PROBLEM + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/Checks.kt b/.teamcity/src/builds/Checks.kt new file mode 100644 index 0000000000000..1228ea4d94f4c --- /dev/null +++ b/.teamcity/src/builds/Checks.kt @@ -0,0 +1,37 @@ +package builds + +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import kibanaAgent + +object Checks : BuildType({ + name = "Checks" + description = "Executes Various Checks" + + kibanaAgent(4) + + val checkScripts = mapOf( + "Check Telemetry Schema" to ".ci/teamcity/checks/telemetry.sh", + "Check TypeScript Projects" to ".ci/teamcity/checks/ts_projects.sh", + "Check File Casing" to ".ci/teamcity/checks/file_casing.sh", + "Check Licenses" to ".ci/teamcity/checks/licenses.sh", + "Verify NOTICE" to ".ci/teamcity/checks/verify_notice.sh", + "Test Hardening" to ".ci/teamcity/checks/test_hardening.sh", + "Check Types" to ".ci/teamcity/checks/type_check.sh", + "Check Doc API Changes" to ".ci/teamcity/checks/doc_api_changes.sh", + "Check Bundle Limits" to ".ci/teamcity/checks/bundle_limits.sh", + "Check i18n" to ".ci/teamcity/checks/i18n.sh" + ) + + steps { + for (checkScript in checkScripts) { + script { + name = checkScript.key + scriptContent = """ + #!/bin/bash + ${checkScript.value} + """.trimIndent() + } + } + } +}) diff --git a/.teamcity/src/builds/FullCi.kt b/.teamcity/src/builds/FullCi.kt new file mode 100644 index 0000000000000..7f19304428d7e --- /dev/null +++ b/.teamcity/src/builds/FullCi.kt @@ -0,0 +1,30 @@ +package builds + +import builds.default.* +import builds.oss.* +import builds.test.AllTests +import dependsOn +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType + +object FullCi : BuildType({ + id("Full_CI") + name = "Full CI" + description = "Runs everything in CI. For tracked branches and PRs." + type = Type.COMPOSITE + + dependsOn( + Lint, + Checks, + AllTests, + OssBuild, + OssAccessibility, + OssPluginFunctional, + OssCiGroups, + OssFirefox, + DefaultBuild, + DefaultCiGroups, + DefaultFirefox, + DefaultAccessibility, + DefaultSecuritySolution + ) +}) diff --git a/.teamcity/src/builds/HourlyCi.kt b/.teamcity/src/builds/HourlyCi.kt new file mode 100644 index 0000000000000..605a22f012976 --- /dev/null +++ b/.teamcity/src/builds/HourlyCi.kt @@ -0,0 +1,34 @@ +package builds + +import addSlackNotifications +import dependsOn +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 + +object HourlyCi : BuildType({ + id("Hourly_CI") + name = "Hourly CI" + description = "Runs everything in CI, hourly" + type = Type.COMPOSITE + + triggers { + schedule { + schedulingPolicy = cron { + hours = "*" + minutes = "0" + } + branchFilter = "refs/heads/master_teamcity" + triggerBuild = always() + withPendingChangesOnly = true + } + } + + dependsOn( + FullCi + ) { + onDependencyCancel = FailureAction.ADD_PROBLEM + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/Lint.kt b/.teamcity/src/builds/Lint.kt new file mode 100644 index 0000000000000..0b3b3b013b5ec --- /dev/null +++ b/.teamcity/src/builds/Lint.kt @@ -0,0 +1,33 @@ +package builds + +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import kibanaAgent + +object Lint : BuildType({ + name = "Lint" + description = "Executes Linting, such as eslint and sasslint" + + kibanaAgent(2) + + steps { + script { + name = "Sasslint" + + scriptContent = + """ + #!/bin/bash + yarn run grunt run:sasslint + """.trimIndent() + } + + script { + name = "ESLint" + scriptContent = + """ + #!/bin/bash + yarn run grunt run:eslint + """.trimIndent() + } + } +}) diff --git a/.teamcity/src/builds/PullRequestCi.kt b/.teamcity/src/builds/PullRequestCi.kt new file mode 100644 index 0000000000000..d3eb697981ce7 --- /dev/null +++ b/.teamcity/src/builds/PullRequestCi.kt @@ -0,0 +1,78 @@ +package builds + +import dependsOn +import jetbrains.buildServer.configs.kotlin.v2019_2.AbsoluteId +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") + name = "Pull Request CI" + type = Type.COMPOSITE + + buildNumberPattern = "%build.counter%-%env.GITHUB_PR_OWNER%-%env.GITHUB_PR_BRANCH%" + + vcs { + root(Kibana) + checkoutDir = "kibana" + + branchFilter = "+:pull/*" + excludeDefaultBranchChanges = true + } + + val prAllowedList = listOf( + "brianseeders", + "alexwizp", + "barlowm", + "DziyanaDzeraviankina", + "maryia-lapata", + "renovate[bot]", + "sulemanof", + "VladLasitsa" + ) + + params { + param("elastic.pull_request.enabled", "true") + param("elastic.pull_request.target_branch", "master_teamcity") + 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(",")) + param("elastic.pull_request.cancel_in_progress_builds_on_update", "true") + + // These params should get filled in by the app that triggers builds + param("env.GITHUB_PR_TARGET_BRANCH", "") + param("env.GITHUB_PR_NUMBER", "") + param("env.GITHUB_PR_OWNER", "") + param("env.GITHUB_PR_REPO", "") + param("env.GITHUB_PR_BRANCH", "") + param("env.GITHUB_PR_TRIGGERED_SHA", "") + param("env.GITHUB_PR_LABELS", "") + param("env.GITHUB_PR_TRIGGER_COMMENT", "") + + param("reverse.dep.*.env.GITHUB_PR_TARGET_BRANCH", "") + param("reverse.dep.*.env.GITHUB_PR_NUMBER", "") + param("reverse.dep.*.env.GITHUB_PR_OWNER", "") + param("reverse.dep.*.env.GITHUB_PR_REPO", "") + param("reverse.dep.*.env.GITHUB_PR_BRANCH", "") + param("reverse.dep.*.env.GITHUB_PR_TRIGGERED_SHA", "") + param("reverse.dep.*.env.GITHUB_PR_LABELS", "") + param("reverse.dep.*.env.GITHUB_PR_TRIGGER_COMMENT", "") + } + + features { + commitStatusPublisher { + vcsRootExtId = "${Kibana.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b" + } + } + } + } + + dependsOn(FullCi) +}) diff --git a/.teamcity/src/builds/default/DefaultAccessibility.kt b/.teamcity/src/builds/default/DefaultAccessibility.kt new file mode 100755 index 0000000000000..f0a9c60cf3e45 --- /dev/null +++ b/.teamcity/src/builds/default/DefaultAccessibility.kt @@ -0,0 +1,12 @@ +package builds.default + +import runbld + +object DefaultAccessibility : DefaultFunctionalBase({ + id("DefaultAccessibility") + name = "Accessibility" + + steps { + runbld("Default Accessibility", "./.ci/teamcity/default/accessibility.sh") + } +}) diff --git a/.teamcity/src/builds/default/DefaultBuild.kt b/.teamcity/src/builds/default/DefaultBuild.kt new file mode 100644 index 0000000000000..f4683e6cf0c1a --- /dev/null +++ b/.teamcity/src/builds/default/DefaultBuild.kt @@ -0,0 +1,56 @@ +package builds.default + +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.Dependencies +import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script + +object DefaultBuild : BuildType({ + name = "Build Default" + description = "Generates Default Build Distribution artifact" + + artifactRules = """ + +:install/kibana/**/* => kibana-default.tar.gz + target/kibana-* + +:src/**/target/public/**/* => kibana-default-plugins.tar.gz!/src/ + +:x-pack/plugins/**/target/public/**/* => kibana-default-plugins.tar.gz!/x-pack/plugins/ + +:x-pack/test/**/target/public/**/* => kibana-default-plugins.tar.gz!/x-pack/test/ + +:examples/**/target/public/**/* => kibana-default-plugins.tar.gz!/examples/ + +:test/**/target/public/**/* => kibana-default-plugins.tar.gz!/test/ + """.trimIndent() + + requirements { + startsWith("teamcity.agent.name", "kibana-c2-16-", "RQ_AGENT_NAME") + } + + steps { + script { + name = "Build Default Distribution" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/default/build.sh + """.trimIndent() + } + } +}) + +fun Dependencies.defaultBuild(rules: String = "+:kibana-default.tar.gz!** => ../build/kibana-build-default") { + dependency(DefaultBuild) { + snapshot { + onDependencyFailure = FailureAction.FAIL_TO_START + onDependencyCancel = FailureAction.FAIL_TO_START + } + + artifacts { + artifactRules = rules + } + } +} + +fun Dependencies.defaultBuildWithPlugins() { + defaultBuild(""" + +:kibana-default.tar.gz!** => ../build/kibana-build-default + +:kibana-default-plugins.tar.gz!** + """.trimIndent()) +} diff --git a/.teamcity/src/builds/default/DefaultCiGroup.kt b/.teamcity/src/builds/default/DefaultCiGroup.kt new file mode 100755 index 0000000000000..7dbe9cd0ba84c --- /dev/null +++ b/.teamcity/src/builds/default/DefaultCiGroup.kt @@ -0,0 +1,15 @@ +package builds.default + +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import runbld + +class DefaultCiGroup(val ciGroup: Int = 0, init: BuildType.() -> Unit = {}) : DefaultFunctionalBase({ + id("DefaultCiGroup_$ciGroup") + name = "CI Group $ciGroup" + + steps { + runbld("Default CI Group $ciGroup", "./.ci/teamcity/default/ci_group.sh $ciGroup") + } + + init() +}) diff --git a/.teamcity/src/builds/default/DefaultCiGroups.kt b/.teamcity/src/builds/default/DefaultCiGroups.kt new file mode 100644 index 0000000000000..6f1d45598c92e --- /dev/null +++ b/.teamcity/src/builds/default/DefaultCiGroups.kt @@ -0,0 +1,15 @@ +package builds.default + +import dependsOn +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType + +const val DEFAULT_CI_GROUP_COUNT = 10 +val defaultCiGroups = (1..DEFAULT_CI_GROUP_COUNT).map { DefaultCiGroup(it) } + +object DefaultCiGroups : BuildType({ + id("Default_CIGroups_Composite") + name = "CI Groups" + type = Type.COMPOSITE + + dependsOn(*defaultCiGroups.toTypedArray()) +}) diff --git a/.teamcity/src/builds/default/DefaultFirefox.kt b/.teamcity/src/builds/default/DefaultFirefox.kt new file mode 100755 index 0000000000000..2429967d24939 --- /dev/null +++ b/.teamcity/src/builds/default/DefaultFirefox.kt @@ -0,0 +1,12 @@ +package builds.default + +import runbld + +object DefaultFirefox : DefaultFunctionalBase({ + id("DefaultFirefox") + name = "Firefox" + + steps { + runbld("Default Firefox", "./.ci/teamcity/default/firefox.sh") + } +}) diff --git a/.teamcity/src/builds/default/DefaultFunctionalBase.kt b/.teamcity/src/builds/default/DefaultFunctionalBase.kt new file mode 100644 index 0000000000000..d8124bd8521c0 --- /dev/null +++ b/.teamcity/src/builds/default/DefaultFunctionalBase.kt @@ -0,0 +1,19 @@ +package builds.default + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType + +open class DefaultFunctionalBase(init: BuildType.() -> Unit = {}) : BuildType({ + params { + param("env.KBN_NP_PLUGINS_BUILT", "true") + } + + dependencies { + defaultBuildWithPlugins() + } + + init() + + addTestSettings() +}) + diff --git a/.teamcity/src/builds/default/DefaultSavedObjectFieldMetrics.kt b/.teamcity/src/builds/default/DefaultSavedObjectFieldMetrics.kt new file mode 100644 index 0000000000000..61505d4757faa --- /dev/null +++ b/.teamcity/src/builds/default/DefaultSavedObjectFieldMetrics.kt @@ -0,0 +1,28 @@ +package builds.default + +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script + +object DefaultSavedObjectFieldMetrics : BuildType({ + id("DefaultSavedObjectFieldMetrics") + name = "Default Saved Object Field Metrics" + + params { + param("env.KBN_NP_PLUGINS_BUILT", "true") + } + + steps { + script { + name = "Default Saved Object Field Metrics" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/default/saved_object_field_metrics.sh + """.trimIndent() + } + } + + dependencies { + defaultBuild() + } +}) diff --git a/.teamcity/src/builds/default/DefaultSecuritySolution.kt b/.teamcity/src/builds/default/DefaultSecuritySolution.kt new file mode 100755 index 0000000000000..1c3b85257c28a --- /dev/null +++ b/.teamcity/src/builds/default/DefaultSecuritySolution.kt @@ -0,0 +1,15 @@ +package builds.default + +import addTestSettings +import runbld + +object DefaultSecuritySolution : DefaultFunctionalBase({ + id("DefaultSecuritySolution") + name = "Security Solution" + + steps { + runbld("Default Security Solution", "./.ci/teamcity/default/security_solution.sh") + } + + addTestSettings() +}) diff --git a/.teamcity/src/builds/es_snapshots/Build.kt b/.teamcity/src/builds/es_snapshots/Build.kt new file mode 100644 index 0000000000000..d0c849ff5f996 --- /dev/null +++ b/.teamcity/src/builds/es_snapshots/Build.kt @@ -0,0 +1,84 @@ +package builds.es_snapshots + +import addSlackNotifications +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import vcs.Elasticsearch +import vcs.Kibana + +object ESSnapshotBuild : BuildType({ + name = "Build Snapshot" + paused = true + + requirements { + startsWith("teamcity.agent.name", "kibana-c2-16-", "RQ_AGENT_NAME") + } + + vcs { + root(Kibana, "+:. => kibana") + root(Elasticsearch, "+:. => elasticsearch") + checkoutDir = "" + } + + params { + param("env.ELASTICSEARCH_BRANCH", "%vcsroot.${Elasticsearch.id.toString()}.branch%") + param("env.ELASTICSEARCH_GIT_COMMIT", "%build.vcs.number.${Elasticsearch.id.toString()}%") + + param("env.GOOGLE_APPLICATION_CREDENTIALS", "%teamcity.build.workingDir%/gcp-credentials.json") + password("env.GOOGLE_APPLICATION_CREDENTIALS_JSON", "credentialsJSON:6e0acb7c-f89c-4225-84b8-4fc102f1a5ef", display = ParameterDisplay.HIDDEN) + } + + steps { + script { + name = "Setup Environment" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/setup_env.sh + """.trimIndent() + } + + script { + name = "Setup Node and Yarn" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/setup_node.sh + """.trimIndent() + } + + script { + name = "Build Elasticsearch Distribution" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/es_snapshots/build.sh + """.trimIndent() + } + + script { + name = "Setup Google Cloud Credentials" + scriptContent = + """#!/bin/bash + echo "${"$"}GOOGLE_APPLICATION_CREDENTIALS_JSON" > "${"$"}GOOGLE_APPLICATION_CREDENTIALS" + gcloud auth activate-service-account --key-file "${"$"}GOOGLE_APPLICATION_CREDENTIALS" + """.trimIndent() + } + + script { + name = "Create Snapshot Manifest" + scriptContent = + """#!/bin/bash + cd kibana + node ./.ci/teamcity/es_snapshots/create_manifest.js "$(cd ../es-build && pwd)" + """.trimIndent() + } + } + + artifactRules = "+:es-build/**/*.json" + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/es_snapshots/Promote.kt b/.teamcity/src/builds/es_snapshots/Promote.kt new file mode 100644 index 0000000000000..9303439d49f30 --- /dev/null +++ b/.teamcity/src/builds/es_snapshots/Promote.kt @@ -0,0 +1,87 @@ +package builds.es_snapshots + +import addSlackNotifications +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.finishBuildTrigger +import vcs.Kibana + +object ESSnapshotPromote : BuildType({ + name = "Promote Snapshot" + paused = true + type = Type.DEPLOYMENT + + vcs { + root(Kibana, "+:. => kibana") + checkoutDir = "" + } + + params { + param("env.ES_SNAPSHOT_MANIFEST", "${ESSnapshotBuild.depParamRefs["env.ES_SNAPSHOT_MANIFEST"]}") + param("env.GOOGLE_APPLICATION_CREDENTIALS", "%teamcity.build.workingDir%/gcp-credentials.json") + password("env.GOOGLE_APPLICATION_CREDENTIALS_JSON", "credentialsJSON:6e0acb7c-f89c-4225-84b8-4fc102f1a5ef", display = ParameterDisplay.HIDDEN) + } + + triggers { + finishBuildTrigger { + buildType = Verify.id.toString() + successfulOnly = true + } + } + + steps { + script { + name = "Setup Environment" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/setup_env.sh + """.trimIndent() + } + + script { + name = "Setup Node and Yarn" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/setup_node.sh + """.trimIndent() + } + + script { + name = "Setup Google Cloud Credentials" + scriptContent = + """#!/bin/bash + echo "${"$"}GOOGLE_APPLICATION_CREDENTIALS_JSON" > "${"$"}GOOGLE_APPLICATION_CREDENTIALS" + gcloud auth activate-service-account --key-file "${"$"}GOOGLE_APPLICATION_CREDENTIALS" + """.trimIndent() + } + + script { + name = "Promote Snapshot Manifest" + scriptContent = + """#!/bin/bash + cd kibana + node ./.ci/teamcity/es_snapshots/promote_manifest.js "${"$"}ES_SNAPSHOT_MANIFEST" + """.trimIndent() + } + } + + dependencies { + dependency(ESSnapshotBuild) { + snapshot { } + + // This is just here to allow build selection in the UI, the file isn't actually used + artifacts { + artifactRules = "manifest.json" + } + } + dependency(Verify) { + snapshot { } + } + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/es_snapshots/PromoteImmediate.kt b/.teamcity/src/builds/es_snapshots/PromoteImmediate.kt new file mode 100644 index 0000000000000..f80a97873b246 --- /dev/null +++ b/.teamcity/src/builds/es_snapshots/PromoteImmediate.kt @@ -0,0 +1,79 @@ +package builds.es_snapshots + +import addSlackNotifications +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.finishBuildTrigger +import vcs.Elasticsearch +import vcs.Kibana + +object ESSnapshotPromoteImmediate : BuildType({ + name = "Immediately Promote Snapshot" + description = "Skip testing and immediately promote the selected snapshot" + paused = true + type = Type.DEPLOYMENT + + vcs { + root(Kibana, "+:. => kibana") + checkoutDir = "" + } + + params { + param("env.ES_SNAPSHOT_MANIFEST", "${ESSnapshotBuild.depParamRefs["env.ES_SNAPSHOT_MANIFEST"]}") + param("env.GOOGLE_APPLICATION_CREDENTIALS", "%teamcity.build.workingDir%/gcp-credentials.json") + password("env.GOOGLE_APPLICATION_CREDENTIALS_JSON", "credentialsJSON:6e0acb7c-f89c-4225-84b8-4fc102f1a5ef", display = ParameterDisplay.HIDDEN) + } + + steps { + script { + name = "Setup Environment" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/setup_env.sh + """.trimIndent() + } + + script { + name = "Setup Node and Yarn" + scriptContent = + """ + #!/bin/bash + cd kibana + ./.ci/teamcity/setup_node.sh + """.trimIndent() + } + + script { + name = "Setup Google Cloud Credentials" + scriptContent = + """#!/bin/bash + echo "${"$"}GOOGLE_APPLICATION_CREDENTIALS_JSON" > "${"$"}GOOGLE_APPLICATION_CREDENTIALS" + gcloud auth activate-service-account --key-file "${"$"}GOOGLE_APPLICATION_CREDENTIALS" + """.trimIndent() + } + + script { + name = "Promote Snapshot Manifest" + scriptContent = + """#!/bin/bash + cd kibana + node ./.ci/teamcity/es_snapshots/promote_manifest.js "${"$"}ES_SNAPSHOT_MANIFEST" + """.trimIndent() + } + } + + dependencies { + dependency(ESSnapshotBuild) { + snapshot { } + + // This is just here to allow build selection in the UI, the file isn't actually used + artifacts { + artifactRules = "manifest.json" + } + } + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/es_snapshots/Verify.kt b/.teamcity/src/builds/es_snapshots/Verify.kt new file mode 100644 index 0000000000000..c778814af536c --- /dev/null +++ b/.teamcity/src/builds/es_snapshots/Verify.kt @@ -0,0 +1,96 @@ +package builds.es_snapshots + +import builds.default.DefaultBuild +import builds.default.DefaultSecuritySolution +import builds.default.defaultCiGroups +import builds.oss.OssBuild +import builds.oss.OssPluginFunctional +import builds.oss.ossCiGroups +import builds.test.ApiServerIntegration +import builds.test.JestIntegration +import dependsOn +import jetbrains.buildServer.configs.kotlin.v2019_2.* + +val cloneForVerify = { build: BuildType -> + val newBuild = BuildType() + build.copyTo(newBuild) + newBuild.id = AbsoluteId(build.id?.toString() + "_ES_Snapshots") + newBuild.params { + param("env.ES_SNAPSHOT_MANIFEST", "${ESSnapshotBuild.depParamRefs["env.ES_SNAPSHOT_MANIFEST"]}") + } + newBuild.dependencies { + dependency(ESSnapshotBuild) { + snapshot { + onDependencyFailure = FailureAction.FAIL_TO_START + onDependencyCancel = FailureAction.FAIL_TO_START + } + // This is just here to allow us to select a build when manually triggering a build using the UI + artifacts { + artifactRules = "manifest.json" + } + } + } + newBuild.steps.items.removeIf { it.name == "Failed Test Reporter" } + newBuild +} + +val ossBuildsToClone = listOf( + *ossCiGroups.toTypedArray(), + OssPluginFunctional +) + +val ossCloned = ossBuildsToClone.map { cloneForVerify(it) } + +val defaultBuildsToClone = listOf( + *defaultCiGroups.toTypedArray(), + DefaultSecuritySolution +) + +val defaultCloned = defaultBuildsToClone.map { cloneForVerify(it) } + +val integrationsBuildsToClone = listOf( + ApiServerIntegration, + JestIntegration +) + +val integrationCloned = integrationsBuildsToClone.map { cloneForVerify(it) } + +object OssTests : BuildType({ + id("ES_Snapshots_OSS_Tests_Composite") + name = "OSS Distro Tests" + type = Type.COMPOSITE + + dependsOn(*ossCloned.toTypedArray()) +}) + +object DefaultTests : BuildType({ + id("ES_Snapshots_Default_Tests_Composite") + name = "Default Distro Tests" + type = Type.COMPOSITE + + dependsOn(*defaultCloned.toTypedArray()) +}) + +object IntegrationTests : BuildType({ + id("ES_Snapshots_Integration_Tests_Composite") + name = "Integration Tests" + type = Type.COMPOSITE + + dependsOn(*integrationCloned.toTypedArray()) +}) + +object Verify : BuildType({ + id("ES_Snapshots_Verify_Composite") + name = "Verify Snapshot" + description = "Run all Kibana functional and integration tests using a given Elasticsearch snapshot" + type = Type.COMPOSITE + + dependsOn( + ESSnapshotBuild, + OssBuild, + DefaultBuild, + OssTests, + DefaultTests, + IntegrationTests + ) +}) diff --git a/.teamcity/src/builds/oss/OssAccessibility.kt b/.teamcity/src/builds/oss/OssAccessibility.kt new file mode 100644 index 0000000000000..8e4a7acd77b76 --- /dev/null +++ b/.teamcity/src/builds/oss/OssAccessibility.kt @@ -0,0 +1,13 @@ +package builds.oss + +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import runbld + +object OssAccessibility : OssFunctionalBase({ + id("OssAccessibility") + name = "Accessibility" + + steps { + runbld("OSS Accessibility", "./.ci/teamcity/oss/accessibility.sh") + } +}) diff --git a/.teamcity/src/builds/oss/OssBuild.kt b/.teamcity/src/builds/oss/OssBuild.kt new file mode 100644 index 0000000000000..50fd73c17ba42 --- /dev/null +++ b/.teamcity/src/builds/oss/OssBuild.kt @@ -0,0 +1,41 @@ +package builds.oss + +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.Dependencies +import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script + +object OssBuild : BuildType({ + name = "Build OSS" + description = "Generates OSS Build Distribution artifact" + + requirements { + startsWith("teamcity.agent.name", "kibana-c2-16-", "RQ_AGENT_NAME") + } + + steps { + script { + name = "Build OSS Distribution" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/oss/build.sh + """.trimIndent() + } + } + + artifactRules = "+:build/oss/kibana-build-oss/**/* => kibana-oss.tar.gz" +}) + +fun Dependencies.ossBuild(rules: String = "+:kibana-oss.tar.gz!** => ../build/kibana-build-oss") { + dependency(OssBuild) { + snapshot { + onDependencyFailure = FailureAction.FAIL_TO_START + onDependencyCancel = FailureAction.FAIL_TO_START + } + + artifacts { + artifactRules = rules + } + } +} diff --git a/.teamcity/src/builds/oss/OssCiGroup.kt b/.teamcity/src/builds/oss/OssCiGroup.kt new file mode 100644 index 0000000000000..1c188cd4c175f --- /dev/null +++ b/.teamcity/src/builds/oss/OssCiGroup.kt @@ -0,0 +1,15 @@ +package builds.oss + +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import runbld + +class OssCiGroup(val ciGroup: Int, init: BuildType.() -> Unit = {}) : OssFunctionalBase({ + id("OssCiGroup_$ciGroup") + name = "CI Group $ciGroup" + + steps { + runbld("OSS CI Group $ciGroup", "./.ci/teamcity/oss/ci_group.sh $ciGroup") + } + + init() +}) diff --git a/.teamcity/src/builds/oss/OssCiGroups.kt b/.teamcity/src/builds/oss/OssCiGroups.kt new file mode 100644 index 0000000000000..931cca2554a24 --- /dev/null +++ b/.teamcity/src/builds/oss/OssCiGroups.kt @@ -0,0 +1,15 @@ +package builds.oss + +import dependsOn +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType + +const val OSS_CI_GROUP_COUNT = 12 +val ossCiGroups = (1..OSS_CI_GROUP_COUNT).map { OssCiGroup(it) } + +object OssCiGroups : BuildType({ + id("OSS_CIGroups_Composite") + name = "CI Groups" + type = Type.COMPOSITE + + dependsOn(*ossCiGroups.toTypedArray()) +}) diff --git a/.teamcity/src/builds/oss/OssFirefox.kt b/.teamcity/src/builds/oss/OssFirefox.kt new file mode 100644 index 0000000000000..2db8314fa44fc --- /dev/null +++ b/.teamcity/src/builds/oss/OssFirefox.kt @@ -0,0 +1,12 @@ +package builds.oss + +import runbld + +object OssFirefox : OssFunctionalBase({ + id("OssFirefox") + name = "Firefox" + + steps { + runbld("OSS Firefox", "./.ci/teamcity/oss/firefox.sh") + } +}) diff --git a/.teamcity/src/builds/oss/OssFunctionalBase.kt b/.teamcity/src/builds/oss/OssFunctionalBase.kt new file mode 100644 index 0000000000000..d8189fd358966 --- /dev/null +++ b/.teamcity/src/builds/oss/OssFunctionalBase.kt @@ -0,0 +1,18 @@ +package builds.oss + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.* + +open class OssFunctionalBase(init: BuildType.() -> Unit = {}) : BuildType({ + params { + param("env.KBN_NP_PLUGINS_BUILT", "true") + } + + dependencies { + ossBuild() + } + + init() + + addTestSettings() +}) diff --git a/.teamcity/src/builds/oss/OssPluginFunctional.kt b/.teamcity/src/builds/oss/OssPluginFunctional.kt new file mode 100644 index 0000000000000..7fbf863820e4c --- /dev/null +++ b/.teamcity/src/builds/oss/OssPluginFunctional.kt @@ -0,0 +1,29 @@ +package builds.oss + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script +import runbld + +object OssPluginFunctional : OssFunctionalBase({ + id("OssPluginFunctional") + name = "Plugin Functional" + + steps { + script { + name = "Build OSS Plugins" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/oss/build_plugins.sh + """.trimIndent() + } + + runbld("OSS Plugin Functional", "./.ci/teamcity/oss/plugin_functional.sh") + } + + dependencies { + ossBuild() + } + + addTestSettings() +}) diff --git a/.teamcity/src/builds/test/AllTests.kt b/.teamcity/src/builds/test/AllTests.kt new file mode 100644 index 0000000000000..d1b5898d1a5f5 --- /dev/null +++ b/.teamcity/src/builds/test/AllTests.kt @@ -0,0 +1,12 @@ +package builds.test + +import dependsOn +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType + +object AllTests : BuildType({ + name = "All Tests" + description = "All Non-Functional Tests" + type = Type.COMPOSITE + + dependsOn(QuickTests, Jest, XPackJest, JestIntegration, ApiServerIntegration) +}) diff --git a/.teamcity/src/builds/test/ApiServerIntegration.kt b/.teamcity/src/builds/test/ApiServerIntegration.kt new file mode 100644 index 0000000000000..d595840c879e6 --- /dev/null +++ b/.teamcity/src/builds/test/ApiServerIntegration.kt @@ -0,0 +1,17 @@ +package builds.test + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import runbld + +object ApiServerIntegration : BuildType({ + name = "API/Server Integration" + description = "Executes API and Server Integration Tests" + + steps { + runbld("API Integration", "yarn run grunt run:apiIntegrationTests") + runbld("Server Integration", "yarn run grunt run:serverIntegrationTests") + } + + addTestSettings() +}) diff --git a/.teamcity/src/builds/test/Jest.kt b/.teamcity/src/builds/test/Jest.kt new file mode 100644 index 0000000000000..04217a4e99b1c --- /dev/null +++ b/.teamcity/src/builds/test/Jest.kt @@ -0,0 +1,19 @@ +package builds.test + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import kibanaAgent +import runbld + +object Jest : BuildType({ + name = "Jest Unit" + description = "Executes Jest Unit Tests" + + kibanaAgent(8) + + steps { + runbld("Jest Unit", "yarn run grunt run:test_jest") + } + + addTestSettings() +}) diff --git a/.teamcity/src/builds/test/JestIntegration.kt b/.teamcity/src/builds/test/JestIntegration.kt new file mode 100644 index 0000000000000..9ec1360dcb1d7 --- /dev/null +++ b/.teamcity/src/builds/test/JestIntegration.kt @@ -0,0 +1,16 @@ +package builds.test + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import runbld + +object JestIntegration : BuildType({ + name = "Jest Integration" + description = "Executes Jest Integration Tests" + + steps { + runbld("Jest Integration", "yarn run grunt run:test_jest_integration") + } + + addTestSettings() +}) diff --git a/.teamcity/src/builds/test/QuickTests.kt b/.teamcity/src/builds/test/QuickTests.kt new file mode 100644 index 0000000000000..1fdb1e366e83f --- /dev/null +++ b/.teamcity/src/builds/test/QuickTests.kt @@ -0,0 +1,29 @@ +package builds.test + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import kibanaAgent +import runbld + +object QuickTests : BuildType({ + name = "Quick Tests" + description = "Executes Quick Tests" + + kibanaAgent(2) + + val testScripts = mapOf( + "Test Hardening" to ".ci/teamcity/tests/test_hardening.sh", + "X-Pack List cyclic dependency" to ".ci/teamcity/tests/xpack_list_cyclic_dependency.sh", + "X-Pack SIEM cyclic dependency" to ".ci/teamcity/tests/xpack_siem_cyclic_dependency.sh", + "Test Projects" to ".ci/teamcity/tests/test_projects.sh", + "Mocha Tests" to ".ci/teamcity/tests/mocha.sh" + ) + + steps { + for (testScript in testScripts) { + runbld(testScript.key, testScript.value) + } + } + + addTestSettings() +}) diff --git a/.teamcity/src/builds/test/XPackJest.kt b/.teamcity/src/builds/test/XPackJest.kt new file mode 100644 index 0000000000000..1958d39183bae --- /dev/null +++ b/.teamcity/src/builds/test/XPackJest.kt @@ -0,0 +1,22 @@ +package builds.test + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import kibanaAgent +import runbld + +object XPackJest : BuildType({ + name = "X-Pack Jest Unit" + description = "Executes X-Pack Jest Unit Tests" + + kibanaAgent(16) + + steps { + runbld("X-Pack Jest Unit", """ + cd x-pack + node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=6 + """.trimIndent()) + } + + addTestSettings() +}) diff --git a/.teamcity/src/projects/EsSnapshots.kt b/.teamcity/src/projects/EsSnapshots.kt new file mode 100644 index 0000000000000..a5aa47d5cae48 --- /dev/null +++ b/.teamcity/src/projects/EsSnapshots.kt @@ -0,0 +1,55 @@ +package projects + +import builds.es_snapshots.* +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import templates.KibanaTemplate + +object EsSnapshotsProject : Project({ + id("ES_Snapshots") + name = "ES Snapshots" + + subProject { + id("ES_Snapshot_Tests") + name = "Tests" + + defaultTemplate = KibanaTemplate + + subProject { + id("ES_Snapshot_Tests_OSS") + name = "OSS Distro Tests" + + ossCloned.forEach { + buildType(it) + } + + buildType(OssTests) + } + + subProject { + id("ES_Snapshot_Tests_Default") + name = "Default Distro Tests" + + defaultCloned.forEach { + buildType(it) + } + + buildType(DefaultTests) + } + + subProject { + id("ES_Snapshot_Tests_Integration") + name = "Integration Tests" + + integrationCloned.forEach { + buildType(it) + } + + buildType(IntegrationTests) + } + } + + buildType(ESSnapshotBuild) + buildType(ESSnapshotPromote) + buildType(ESSnapshotPromoteImmediate) + buildType(Verify) +}) diff --git a/.teamcity/src/projects/Kibana.kt b/.teamcity/src/projects/Kibana.kt new file mode 100644 index 0000000000000..20c30eedf5b91 --- /dev/null +++ b/.teamcity/src/projects/Kibana.kt @@ -0,0 +1,171 @@ +package projects + +import vcs.Kibana +import builds.* +import builds.default.* +import builds.oss.* +import builds.test.* +import jetbrains.buildServer.configs.kotlin.v2019_2.* +import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.slackConnection +import kibanaAgent +import templates.KibanaTemplate +import templates.DefaultTemplate +import vcs.Elasticsearch + +class KibanaConfiguration() { + var agentNetwork: String = "teamcity" + var agentSubnet: String = "teamcity" + + constructor(init: KibanaConfiguration.() -> Unit) : this() { + init() + } +} + +var kibanaConfiguration = KibanaConfiguration() + +fun Kibana(config: KibanaConfiguration = KibanaConfiguration()) : Project { + kibanaConfiguration = config + + return Project { + params { + param("teamcity.ui.settings.readOnly", "true") + + // https://github.com/JetBrains/teamcity-webhooks + 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") + password("teamcity.internal.webhooks.password", "credentialsJSON:b2ee34c5-fc89-4596-9b47-ecdeb68e4e7a", display = ParameterDisplay.HIDDEN) + } + + vcsRoot(Kibana) + vcsRoot(Elasticsearch) + + template(DefaultTemplate) + template(KibanaTemplate) + + defaultTemplate = DefaultTemplate + + features { + val sizes = listOf("2", "4", "8", "16") + for (size in sizes) { + kibanaAgent(size) + } + + kibanaAgent { + id = "KIBANA_C2_16" + param("source-id", "kibana-c2-16-") + param("machineType", "c2-standard-16") + } + + feature { + id = "kibana" + type = "CloudProfile" + param("agentPushPreset", "") + param("profileId", "kibana") + param("profileServerUrl", "") + param("name", "kibana") + param("total-work-time", "") + param("credentialsType", "key") + param("description", "") + param("next-hour", "") + param("cloud-code", "google") + param("terminate-after-build", "true") + param("terminate-idle-time", "30") + param("enabled", "true") + param("secure:accessKey", "credentialsJSON:447fdd4d-7129-46b7-9822-2e57658c7422") + } + + slackConnection { + id = "KIBANA_SLACK" + displayName = "Kibana Slack" + botToken = "credentialsJSON:39eafcfc-97a6-4877-82c1-115f1f10d14b" + clientId = "12985172978.1291178427153" + clientSecret = "credentialsJSON:8b5901fb-fd2c-4e45-8aff-fdd86dc68f67" + } + } + + subProject { + id("CI") + name = "CI" + defaultTemplate = KibanaTemplate + + buildType(Lint) + buildType(Checks) + + subProject { + id("Test") + name = "Test" + + subProject { + id("Jest") + name = "Jest" + + buildType(Jest) + buildType(XPackJest) + buildType(JestIntegration) + } + + buildType(ApiServerIntegration) + buildType(QuickTests) + buildType(AllTests) + } + + subProject { + id("OSS") + name = "OSS Distro" + + buildType(OssBuild) + + subProject { + id("OSS_Functional") + name = "Functional" + + buildType(OssCiGroups) + buildType(OssFirefox) + buildType(OssAccessibility) + buildType(OssPluginFunctional) + + subProject { + id("CIGroups") + name = "CI Groups" + + ossCiGroups.forEach { buildType(it) } + } + } + } + + subProject { + id("Default") + name = "Default Distro" + + buildType(DefaultBuild) + + subProject { + id("Default_Functional") + name = "Functional" + + buildType(DefaultCiGroups) + buildType(DefaultFirefox) + buildType(DefaultAccessibility) + buildType(DefaultSecuritySolution) + buildType(DefaultSavedObjectFieldMetrics) + + subProject { + id("Default_CIGroups") + name = "CI Groups" + + defaultCiGroups.forEach { buildType(it) } + } + } + } + + buildType(FullCi) + buildType(BaselineCi) + buildType(HourlyCi) + buildType(PullRequestCi) + } + + subProject(EsSnapshotsProject) + } +} diff --git a/.teamcity/src/templates/DefaultTemplate.kt b/.teamcity/src/templates/DefaultTemplate.kt new file mode 100644 index 0000000000000..762218b72ab10 --- /dev/null +++ b/.teamcity/src/templates/DefaultTemplate.kt @@ -0,0 +1,25 @@ +package templates + +import jetbrains.buildServer.configs.kotlin.v2019_2.Template +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.perfmon + +object DefaultTemplate : Template({ + name = "Default Template" + + requirements { + equals("system.cloud.profile_id", "kibana", "RQ_CLOUD_PROFILE_ID") + startsWith("teamcity.agent.name", "kibana-standard-2-", "RQ_AGENT_NAME") + } + + params { + param("env.HOME", "/var/lib/jenkins") // TODO once the agent images are sorted out + } + + features { + perfmon { } + } + + failureConditions { + executionTimeoutMin = 120 + } +}) diff --git a/.teamcity/src/templates/KibanaTemplate.kt b/.teamcity/src/templates/KibanaTemplate.kt new file mode 100644 index 0000000000000..117c30ddb86e3 --- /dev/null +++ b/.teamcity/src/templates/KibanaTemplate.kt @@ -0,0 +1,141 @@ +package templates + +import vcs.Kibana +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildStep +import jetbrains.buildServer.configs.kotlin.v2019_2.ParameterDisplay +import jetbrains.buildServer.configs.kotlin.v2019_2.Template +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.PullRequests +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.perfmon +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.pullRequests +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.placeholder +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script + +object KibanaTemplate : Template({ + name = "Kibana Template" + description = "For builds that need to check out kibana and execute against the repo using node" + + vcs { + root(Kibana) + + checkoutDir = "kibana" +// checkoutDir = "/dev/shm/%system.teamcity.buildType.id%/%system.build.number%/kibana" + } + + requirements { + equals("system.cloud.profile_id", "kibana", "RQ_CLOUD_PROFILE_ID") + startsWith("teamcity.agent.name", "kibana-standard-2-", "RQ_AGENT_NAME") + } + + features { + perfmon { } + pullRequests { + vcsRootExtId = "${Kibana.id}" + provider = github { + authType = token { + token = "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b" + } + filterTargetBranch = "refs/heads/master_teamcity" + filterAuthorRole = PullRequests.GitHubRoleFilter.MEMBER + } + } + } + + failureConditions { + executionTimeoutMin = 120 + testFailure = false + } + + params { + param("env.CI", "true") + param("env.TEAMCITY_CI", "true") + param("env.HOME", "/var/lib/jenkins") // TODO once the agent images are sorted out + + // TODO remove these + param("env.GCS_UPLOAD_PREFIX", "INVALID_PREFIX") + param("env.CI_PARALLEL_PROCESS_NUMBER", "1") + + param("env.TEAMCITY_URL", "%teamcity.serverUrl%") + param("env.TEAMCITY_BUILD_URL", "%teamcity.serverUrl%/build/%teamcity.build.id%") + param("env.TEAMCITY_JOB_ID", "%system.teamcity.buildType.id%") + param("env.TEAMCITY_BUILD_ID", "%build.number%") + param("env.TEAMCITY_BUILD_NUMBER", "%teamcity.build.id%") + + param("env.GIT_BRANCH", "%vcsroot.branch%") + param("env.GIT_COMMIT", "%build.vcs.number%") + param("env.branch_specifier", "%vcsroot.branch%") + + 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) + + // TODO move these to vault once the configuration is finalized + // password("env.CI_STATS_TOKEN", "%vault:kibana-issues:secret/kibana-issues/dev/kibana_ci_stats!/api_token%", display = ParameterDisplay.HIDDEN) + // 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) + + } + + steps { + script { + name = "Setup Environment" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/setup_env.sh + """.trimIndent() + } + + script { + name = "Setup Node and Yarn" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/setup_node.sh + """.trimIndent() + } + + script { + name = "Setup CI Stats" + scriptContent = + """ + #!/bin/bash + node .ci/teamcity/setup_ci_stats.js + """.trimIndent() + } + + script { + name = "Bootstrap" + scriptContent = + """ + #!/bin/bash + ./.ci/teamcity/bootstrap.sh + """.trimIndent() + } + + placeholder {} + + script { + name = "Set Build Status Success" + scriptContent = + """ + #!/bin/bash + echo "##teamcity[setParameter name='env.BUILD_STATUS' value='SUCCESS']" + """.trimIndent() + executionMode = BuildStep.ExecutionMode.RUN_ON_SUCCESS + } + + script { + name = "CI Stats Complete" + scriptContent = + """ + #!/bin/bash + node .ci/teamcity/ci_stats_complete.js + """.trimIndent() + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + } + } +}) diff --git a/.teamcity/src/vcs/Elasticsearch.kt b/.teamcity/src/vcs/Elasticsearch.kt new file mode 100644 index 0000000000000..ab7120b854446 --- /dev/null +++ b/.teamcity/src/vcs/Elasticsearch.kt @@ -0,0 +1,11 @@ +package vcs + +import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot + +object Elasticsearch : GitVcsRoot({ + id("elasticsearch_master") + + name = "elasticsearch / master" + url = "https://github.com/elastic/elasticsearch.git" + branch = "refs/heads/master" +}) diff --git a/.teamcity/src/vcs/Kibana.kt b/.teamcity/src/vcs/Kibana.kt new file mode 100644 index 0000000000000..d847a1565e6e0 --- /dev/null +++ b/.teamcity/src/vcs/Kibana.kt @@ -0,0 +1,11 @@ +package vcs + +import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot + +object Kibana : GitVcsRoot({ + id("kibana_master") + + name = "kibana / master" + url = "https://github.com/elastic/kibana.git" + branch = "refs/heads/master_teamcity" +}) diff --git a/.teamcity/tests/projects/KibanaTest.kt b/.teamcity/tests/projects/KibanaTest.kt new file mode 100644 index 0000000000000..677effec5be65 --- /dev/null +++ b/.teamcity/tests/projects/KibanaTest.kt @@ -0,0 +1,27 @@ +package projects + +import org.junit.Assert.* +import org.junit.Test + +val TestConfig = KibanaConfiguration { + agentNetwork = "network" + agentSubnet = "subnet" +} + +class KibanaTest { + @Test + fun test_Default_Configuration_Exists() { + assertNotNull(kibanaConfiguration) + Kibana() + assertEquals("teamcity", kibanaConfiguration.agentNetwork) + } + + @Test + fun test_CloudImages_Exist() { + val project = Kibana(TestConfig) + + assertTrue(project.features.items.any { + it.type == "CloudImage" && it.params.any { param -> param.name == "network" && param.value == "network"} + }) + } +} diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customlabel.md similarity index 58% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customlabel.md index b30201f9e3991..6a997d517e98d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customlabel.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [customLabel](./kibana-plugin-plugins-data-public.ifieldtype.customlabel.md) -## IFieldType.customName property +## IFieldType.customLabel property Signature: ```typescript -customName?: string; +customLabel?: string; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 6f3876ff82f04..2b3d3df1ec8d0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -16,7 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-public.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-public.ifieldtype.count.md) | number | | -| [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) | string | | +| [customLabel](./kibana-plugin-plugins-data-public.ifieldtype.customlabel.md) | string | | | [displayName](./kibana-plugin-plugins-data-public.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-public.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-public.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md index f81edf4b94b42..0c1fbe7d0d1b6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md @@ -9,7 +9,7 @@ ```typescript getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 1228bf7adc2ef..3383116f404b2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -27,7 +27,7 @@ export declare class IndexPattern implements IIndexPattern | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customLabel: string;
};
} | | | [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md similarity index 59% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md index ef8f9f1d31e4f..8d9c1b7a1161e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [customLabel](./kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md) -## IndexPatternField.customName property +## IndexPatternField.customLabel property Signature: ```typescript -get customName(): string | undefined; +get customLabel(): string | undefined; -set customName(label: string | undefined); +set customLabel(customLabel: string | undefined); ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index ef99b4353a70b..caf7d374161dd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -23,7 +23,7 @@ export declare class IndexPatternField implements IFieldType | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | | [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | Description of field type conflicts across different indices in the same index pattern | | [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | Count is used for field popularity | -| [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) | | string | undefined | | +| [customLabel](./kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md) | | string | undefined | | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md index c7237701ae49d..f0600dd20658a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md @@ -20,7 +20,7 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - customName: string | undefined; + customLabel: string | undefined; }; ``` Returns: @@ -38,6 +38,6 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - customName: string | undefined; + customLabel: string | undefined; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customlabel.md similarity index 58% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customlabel.md index f5fbc084237f2..8d4868cb8e9ab 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customlabel.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [customLabel](./kibana-plugin-plugins-data-server.ifieldtype.customlabel.md) -## IFieldType.customName property +## IFieldType.customLabel property Signature: ```typescript -customName?: string; +customLabel?: string; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 638700b1d24f8..48836a1b620b8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -16,7 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-server.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-server.ifieldtype.count.md) | number | | -| [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) | string | | +| [customLabel](./kibana-plugin-plugins-data-server.ifieldtype.customlabel.md) | string | | | [displayName](./kibana-plugin-plugins-data-server.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-server.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-server.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md index 80dd329232ed8..b1e38258353c3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md @@ -9,7 +9,7 @@ ```typescript getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 3d2b021b29515..5103af52f1b43 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -27,7 +27,7 @@ export declare class IndexPattern implements IIndexPattern | [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customLabel: string;
};
} | | | [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index 35f1160ee834d..c1d287fca1f44 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -147,7 +147,7 @@ To match multiple fields: machine.os*:windows 10 ------------------- -This sytax is handy when you have text and keyword +This syntax is handy when you have text and keyword versions of a field. The query checks machine.os and machine.os.keyword for the term `windows 10`. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index dbac6997ff433..6244a43b54f72 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -454,7 +454,8 @@ of buckets to try to represent. [horizontal] [[visualization-colormapping]]`visualization:colorMapping`:: -Maps values to specified colors in visualizations. +**This setting is deprecated and will not be supported as of 8.0.** +Maps values to specific colors in *Visualize* charts and *TSVB*. This setting does not apply to *Lens*. [[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed when highlighting another element diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index ef76121b21d29..649d4fe951263 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -36,6 +36,12 @@ for example, `logstash-*`. === Settings changes // tag::notable-breaking-changes[] +[float] +==== Multitenancy by changing `kibana.index` is no longer supported +*Details:* `kibana.index`, `xpack.reporting.index` and `xpack.task_manager.index` can no longer be specified. + +*Impact:* Users who relied on changing these settings to achieve multitenancy should use *Spaces*, cross-cluster replication, or cross-cluster search instead. To migrate to *Spaces*, users are encouraged to use saved object management to export their saved objects from a tenant into the default tenant in a space. Improvements are planned to improve on this workflow. See https://github.com/elastic/kibana/issues/82020 for more details. + [float] ==== Legacy browsers are now rejected by default *Details:* `csp.strict` is now enabled by default, so Kibana will fail to load for older, legacy browsers that do not enforce basic Content Security Policy protections - notably Internet Explorer 11. diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index af80b17f8605f..599cce3a03cd9 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -23,6 +23,9 @@ a| <> | Create an incident in Jira. +a| <> + +| Send a message to a Microsoft Teams channel. a| <> @@ -65,6 +68,7 @@ include::action-types/email.asciidoc[] include::action-types/resilient.asciidoc[] include::action-types/index.asciidoc[] include::action-types/jira.asciidoc[] +include::action-types/teams.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/servicenow.asciidoc[] diff --git a/docs/user/alerting/action-types/teams.asciidoc b/docs/user/alerting/action-types/teams.asciidoc new file mode 100644 index 0000000000000..6706dd2e5643f --- /dev/null +++ b/docs/user/alerting/action-types/teams.asciidoc @@ -0,0 +1,58 @@ +[role="xpack"] +[[teams-action-type]] +=== Microsoft Teams action + +The Microsoft Teams action type uses https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Incoming Webhooks]. + +[float] +[[teams-connector-configuration]] +==== Connector configuration + +Microsoft Teams connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#add-an-incoming-webhook-to-a-teams-channel[Add Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is added to the allowed hosts. + +[float] +[[Preconfigured-teams-configuration]] +==== Preconfigured action type + +[source,text] +-- + my-teams: + name: preconfigured-teams-action-type + actionTypeId: .teams + config: + webhookUrl: 'https://outlook.office.com/webhook/abcd@0123456/IncomingWebhook/abcdefgh/ijklmnopqrstuvwxyz' +-- + +`config` defines the action type specific to the configuration. +`config` contains +`webhookUrl`, a string that corresponds to *Webhook URL*. + + +[float] +[[teams-action-configuration]] +==== Action configuration + +Microsoft Teams actions have the following properties: + +Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. + +[[configuring-teams]] +==== Configuring Microsoft Teams Accounts + +You need a https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Microsoft Teams webhook URL] to +configure a Microsoft Teams action. To create a webhook +URL, add the **Incoming Webhook App** through the Microsoft Teams console: + +. Log in to http://teams.microsoft.com[teams.microsoft.com] as a team administrator. +. Navigate to the Apps directory, search for and select the *Incoming Webhook* app. +. Choose _Add to team_ and select a team and channel for the app. +. Enter a name for your webhook and (optionally) upload a custom icon. ++ +image::images/teams-add-webhook-integration.png[] +. Click *Create*. +. Copy the generated webhook URL so you can paste it into your Teams connector form. ++ +image::images/teams-copy-webhook-url.png[] diff --git a/docs/user/alerting/images/teams-add-webhook-integration.png b/docs/user/alerting/images/teams-add-webhook-integration.png new file mode 100644 index 0000000000000..a2d070cb33743 Binary files /dev/null and b/docs/user/alerting/images/teams-add-webhook-integration.png differ diff --git a/docs/user/alerting/images/teams-copy-webhook-url.png b/docs/user/alerting/images/teams-copy-webhook-url.png new file mode 100644 index 0000000000000..adb455c64cbf0 Binary files /dev/null and b/docs/user/alerting/images/teams-copy-webhook-url.png differ diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index ca788020d9286..1b9896d7dea56 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -3,7 +3,7 @@ == Create custom dashboard actions Custom dashboard actions, also known as drilldowns, allow you to create -workflows for analyzing and troubleshooting your data. Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all of the panels. Each panel can have multiple drilldowns. +workflows for analyzing and troubleshooting your data. Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all of the panels. Each panel can have multiple drilldowns. Third-party developers can create drilldowns. To learn how to code drilldowns, refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin]. @@ -28,7 +28,7 @@ Dashboard drilldowns enable you to open a dashboard from another dashboard, taking the time range, filters, and other parameters with you, so the context remains the same. Dashboard drilldowns help you to continue your analysis from a new perspective. -For example, if you have a dashboard that shows the overall status of multiple data center, +For example, if you have a dashboard that shows the overall status of multiple data center, you can create a drilldown that navigates from the overall status dashboard to a dashboard that shows a single data center or server. @@ -41,14 +41,14 @@ Destination URLs can be dynamic, depending on the dashboard context or user inte For example, if you have a dashboard that shows data from a Github repository, you can create a URL drilldown that opens Github from the dashboard. -Some panels support multiple interactions, also known as triggers. +Some panels support multiple interactions, also known as triggers. The <> you use to create a <> depends on the trigger you choose. URL drilldowns support these types of triggers: * *Single click* — A single data point in the visualization. * *Range selection* — A range of values in a visualization. -For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. +For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. To disable URL drilldowns on your {kib} instance, disable the plugin: @@ -77,20 +77,20 @@ The following panels support dashboard and URL drilldowns. ^| X | Controls -^| -^| +^| +^| | Data Table ^| X ^| X | Gauge -^| -^| +^| +^| | Goal -^| -^| +^| +^| | Heat map ^| X @@ -106,15 +106,15 @@ The following panels support dashboard and URL drilldowns. | Maps ^| X -^| +^| X | Markdown -^| -^| +^| +^| | Metric -^| -^| +^| +^| | Pie ^| X @@ -122,7 +122,7 @@ The following panels support dashboard and URL drilldowns. | TSVB ^| X -^| +^| | Tag Cloud ^| X @@ -130,11 +130,11 @@ The following panels support dashboard and URL drilldowns. | Timelion ^| X -^| +^| | Vega ^| X -^| +^| | Vertical Bar ^| X @@ -192,7 +192,7 @@ image::images/drilldown_create.png[Create drilldown with entries for drilldown n . Click *Create drilldown*. + -The drilldown is stored as dashboard metadata. +The drilldown is stored as dashboard metadata. . Save the dashboard. + @@ -226,7 +226,7 @@ image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigate .. Select *Go to URL*. -.. Enter the URL template: +.. Enter the URL template: + [source, bash] ---- @@ -240,7 +240,7 @@ image:images/url_drilldown_url_template.png[URL template input] .. Click *Create drilldown*. + -The drilldown is stored as dashboard metadata. +The drilldown is stored as dashboard metadata. . Save the dashboard. + diff --git a/package.json b/package.json index d33135d37e1e6..0265250842756 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@kbn/config-schema": "link:packages/kbn-config-schema", "@kbn/i18n": "link:packages/kbn-i18n", "@kbn/interpreter": "link:packages/kbn-interpreter", + "@kbn/legacy-logging": "link:packages/kbn-legacy-logging", "@kbn/logging": "link:packages/kbn-logging", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/std": "link:packages/kbn-std", @@ -419,7 +420,6 @@ "@types/cmd-shim": "^2.0.0", "@types/color": "^3.0.0", "@types/compression-webpack-plugin": "^2.0.2", - "@types/console-stamp": "^0.2.32", "@types/cypress-cucumber-preprocessor": "^1.14.1", "@types/cytoscape": "^3.14.0", "@types/d3": "^3.5.43", @@ -602,7 +602,6 @@ "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compare-versions": "3.5.1", - "console-stamp": "^0.2.9", "constate": "^1.3.2", "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", @@ -693,15 +692,15 @@ "is-glob": "^4.0.1", "is-path-inside": "^3.0.2", "istanbul-instrumenter-loader": "^3.0.1", - "jest": "^26.4.2", + "jest": "^26.6.3", "jest-canvas-mock": "^2.2.0", - "jest-circus": "^26.4.2", - "jest-cli": "^26.4.2", - "jest-diff": "^26.4.2", + "jest-circus": "^26.6.3", + "jest-cli": "^26.6.3", + "jest-diff": "^26.6.2", "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jest-silent-reporter": "^0.2.1", - "jest-snapshot": "^26.4.2", + "jest-snapshot": "^26.6.2", "jest-specific-snapshot": "2.0.0", "jest-styled-components": "^7.0.2", "jest-when": "^2.7.2", @@ -723,7 +722,7 @@ "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^0.6.10", + "lmdb-store": "^0.8.15", "load-grunt-config": "^3.0.1", "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", diff --git a/packages/kbn-i18n/src/react/index.tsx b/packages/kbn-i18n/src/react/index.tsx index b438c44598b75..857a2b0824b55 100644 --- a/packages/kbn-i18n/src/react/index.tsx +++ b/packages/kbn-i18n/src/react/index.tsx @@ -18,9 +18,7 @@ */ import { InjectedIntl as _InjectedIntl, InjectedIntlProps as _InjectedIntlProps } from 'react-intl'; - -export type InjectedIntl = _InjectedIntl; -export type InjectedIntlProps = _InjectedIntlProps; +export type { InjectedIntl, InjectedIntlProps } from 'react-intl'; export { intlShape, diff --git a/packages/kbn-legacy-logging/README.md b/packages/kbn-legacy-logging/README.md new file mode 100644 index 0000000000000..4c5989fc892dc --- /dev/null +++ b/packages/kbn-legacy-logging/README.md @@ -0,0 +1,4 @@ +# @kbn/legacy-logging + +This package contains the implementation of the legacy logging +system, based on `@hapi/good` \ No newline at end of file diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json new file mode 100644 index 0000000000000..9311b3e2a77b3 --- /dev/null +++ b/packages/kbn-legacy-logging/package.json @@ -0,0 +1,15 @@ +{ + "name": "@kbn/legacy-logging", + "version": "1.0.0", + "private": true, + "license": "Apache-2.0", + "main": "./target/index.js", + "scripts": { + "build": "tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@kbn/std": "link:../kbn-std" + } +} diff --git a/src/legacy/server/logging/configuration.js b/packages/kbn-legacy-logging/src/get_logging_config.ts similarity index 74% rename from src/legacy/server/logging/configuration.js rename to packages/kbn-legacy-logging/src/get_logging_config.ts index 267dc9a334de8..cf49177e50b7b 100644 --- a/src/legacy/server/logging/configuration.js +++ b/packages/kbn-legacy-logging/src/get_logging_config.ts @@ -18,20 +18,25 @@ */ import _ from 'lodash'; -import { getLoggerStream } from './log_reporter'; +import { getLogReporter } from './log_reporter'; +import { LegacyLoggingConfig } from './schema'; -export default function loggingConfiguration(config) { - const events = config.get('logging.events'); +/** + * Returns the `@hapi/good` plugin configuration to be used for the legacy logging + * @param config + */ +export function getLoggingConfiguration(config: LegacyLoggingConfig, opsInterval: number) { + const events = config.events; - if (config.get('logging.silent')) { + if (config.silent) { _.defaults(events, {}); - } else if (config.get('logging.quiet')) { + } else if (config.quiet) { _.defaults(events, { log: ['listening', 'error', 'fatal'], request: ['error'], error: '*', }); - } else if (config.get('logging.verbose')) { + } else if (config.verbose) { _.defaults(events, { log: '*', ops: '*', @@ -47,24 +52,24 @@ export default function loggingConfiguration(config) { }); } - const loggerStream = getLoggerStream({ + const loggerStream = getLogReporter({ config: { - json: config.get('logging.json'), - dest: config.get('logging.dest'), - timezone: config.get('logging.timezone'), + json: config.json, + dest: config.dest, + timezone: config.timezone, // I'm adding the default here because if you add another filter // using the commandline it will remove authorization. I want users // to have to explicitly set --logging.filter.authorization=none or // --logging.filter.cookie=none to have it show up in the logs. - filter: _.defaults(config.get('logging.filter'), { + filter: _.defaults(config.filter, { authorization: 'remove', cookie: 'remove', }), }, events: _.transform( events, - function (filtered, val, key) { + function (filtered: Record, val: string, key: string) { // provide a string compatible way to remove events if (val !== '!') filtered[key] = val; }, @@ -74,7 +79,7 @@ export default function loggingConfiguration(config) { const options = { ops: { - interval: config.get('ops.interval'), + interval: opsInterval, }, includes: { request: ['headers', 'payload'], diff --git a/packages/kbn-legacy-logging/src/index.ts b/packages/kbn-legacy-logging/src/index.ts new file mode 100644 index 0000000000000..0fa5f65abf861 --- /dev/null +++ b/packages/kbn-legacy-logging/src/index.ts @@ -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. + */ + +export { LegacyLoggingConfig, legacyLoggingConfigSchema } from './schema'; +export { attachMetaData } from './metadata'; +export { setupLoggingRotate } from './rotate'; +export { setupLogging, reconfigureLogging } from './setup_logging'; +export { getLoggingConfiguration } from './get_logging_config'; +export { LegacyLoggingServer } from './legacy_logging_server'; diff --git a/src/core/server/legacy/logging/legacy_logging_server.test.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts similarity index 86% rename from src/core/server/legacy/logging/legacy_logging_server.test.ts rename to packages/kbn-legacy-logging/src/legacy_logging_server.test.ts index 2f6c34e0fc5d6..9b1ba87c250dc 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.test.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts @@ -17,11 +17,9 @@ * under the License. */ -jest.mock('../../../../legacy/server/config'); -jest.mock('../../../../legacy/server/logging'); +jest.mock('./setup_logging'); -import { LogLevel } from '../../logging'; -import { LegacyLoggingServer } from './legacy_logging_server'; +import { LegacyLoggingServer, LogRecord } from './legacy_logging_server'; test('correctly forwards log records.', () => { const loggingServer = new LegacyLoggingServer({ events: {} }); @@ -29,28 +27,37 @@ test('correctly forwards log records.', () => { loggingServer.events.on('log', onLogMock); const timestamp = 1554433221100; - const firstLogRecord = { + const firstLogRecord: LogRecord = { timestamp: new Date(timestamp), pid: 5355, - level: LogLevel.Info, + level: { + id: 'info', + value: 5, + }, context: 'some-context', message: 'some-message', }; - const secondLogRecord = { + const secondLogRecord: LogRecord = { timestamp: new Date(timestamp), pid: 5355, - level: LogLevel.Error, + level: { + id: 'error', + value: 3, + }, context: 'some-context.sub-context', message: 'some-message', meta: { unknown: 2 }, error: new Error('some-error'), }; - const thirdLogRecord = { + const thirdLogRecord: LogRecord = { timestamp: new Date(timestamp), pid: 5355, - level: LogLevel.Trace, + level: { + id: 'trace', + value: 7, + }, context: 'some-context.sub-context', message: 'some-message', meta: { tags: ['important', 'tags'], unknown: 2 }, diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts similarity index 73% rename from src/core/server/legacy/logging/legacy_logging_server.ts rename to packages/kbn-legacy-logging/src/legacy_logging_server.ts index 690c9c0bfe21d..45e4bda0b007c 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -17,29 +17,40 @@ * under the License. */ -import { ServerExtType } from '@hapi/hapi'; -import Podium from '@hapi/podium'; -// @ts-expect-error: implicit any for JS file -import { Config } from '../../../../legacy/server/config'; -// @ts-expect-error: implicit any for JS file -import { setupLogging } from '../../../../legacy/server/logging'; -import { LogLevel, LogRecord } from '../../logging'; -import { LegacyVars } from '../../types'; - -export const metadataSymbol = Symbol('log message with metadata'); -export function attachMetaData(message: string, metadata: LegacyVars = {}) { - return { - [metadataSymbol]: { - message, - metadata, - }, - }; +import { ServerExtType, Server } from '@hapi/hapi'; +import Podium from 'podium'; +import { setupLogging } from './setup_logging'; +import { attachMetaData } from './metadata'; +import { legacyLoggingConfigSchema } from './schema'; + +// these LogXXX types are duplicated to avoid a cross dependency with the @kbn/logging package. +// typescript will error if they diverge at some point. +type LogLevelId = 'all' | 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'off'; + +interface LogLevel { + id: LogLevelId; + value: number; +} + +export interface LogRecord { + timestamp: Date; + level: LogLevel; + context: string; + message: string; + error?: Error; + meta?: { [name: string]: any }; + pid: number; } + const isEmptyObject = (obj: object) => Object.keys(obj).length === 0; function getDataToLog(error: Error | undefined, metadata: object, message: string) { - if (error) return error; - if (!isEmptyObject(metadata)) return attachMetaData(message, metadata); + if (error) { + return error; + } + if (!isEmptyObject(metadata)) { + return attachMetaData(message, metadata); + } return message; } @@ -50,7 +61,7 @@ interface PluginRegisterParams { options: PluginRegisterParams['options'] ) => Promise; }; - options: LegacyVars; + options: Record; } /** @@ -84,22 +95,19 @@ export class LegacyLoggingServer { private onPostStopCallback?: () => void; - constructor(legacyLoggingConfig: Readonly) { + constructor(legacyLoggingConfig: any) { // We set `ops.interval` to max allowed number and `ops` filter to value // that doesn't exist to avoid logging of ops at all, if turned on it will be // logged by the "legacy" Kibana. - const config = { - logging: { - ...legacyLoggingConfig, - events: { - ...legacyLoggingConfig.events, - ops: '__no-ops__', - }, + const { value: loggingConfig } = legacyLoggingConfigSchema.validate({ + ...legacyLoggingConfig, + events: { + ...legacyLoggingConfig.events, + ops: '__no-ops__', }, - ops: { interval: 2147483647 }, - }; + }); - setupLogging(this, Config.withDefaultSchema(config)); + setupLogging((this as unknown) as Server, loggingConfig, 2147483647); } public register({ plugin: { register }, options }: PluginRegisterParams): Promise { diff --git a/packages/kbn-legacy-logging/src/log_events.ts b/packages/kbn-legacy-logging/src/log_events.ts new file mode 100644 index 0000000000000..296c255a75185 --- /dev/null +++ b/packages/kbn-legacy-logging/src/log_events.ts @@ -0,0 +1,80 @@ +/* + * 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 { EventData, isEventData } from './metadata'; + +export interface BaseEvent { + event: string; + timestamp: number; + pid: number; + tags?: string[]; +} + +export interface ResponseEvent extends BaseEvent { + event: 'response'; + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + statusCode: number; + path: string; + headers: Record; + responsePayload: string; + responseTime: string; + query: Record; +} + +export interface OpsEvent extends BaseEvent { + event: 'ops'; + os: { + load: string[]; + }; + proc: Record; + load: string; +} + +export interface ErrorEvent extends BaseEvent { + event: 'error'; + error: Error; + url: string; +} + +export interface UndeclaredErrorEvent extends BaseEvent { + error: Error; +} + +export interface LogEvent extends BaseEvent { + data: EventData; +} + +export interface UnkownEvent extends BaseEvent { + data: string | Record; +} + +export type AnyEvent = + | ResponseEvent + | OpsEvent + | ErrorEvent + | UndeclaredErrorEvent + | LogEvent + | UnkownEvent; + +export const isResponseEvent = (e: AnyEvent): e is ResponseEvent => e.event === 'response'; +export const isOpsEvent = (e: AnyEvent): e is OpsEvent => e.event === 'ops'; +export const isErrorEvent = (e: AnyEvent): e is ErrorEvent => e.event === 'error'; +export const isLogEvent = (e: AnyEvent): e is LogEvent => isEventData((e as LogEvent).data); +export const isUndeclaredErrorEvent = (e: AnyEvent): e is UndeclaredErrorEvent => + (e as any).error instanceof Error; diff --git a/src/legacy/server/logging/log_format.js b/packages/kbn-legacy-logging/src/log_format.ts similarity index 61% rename from src/legacy/server/logging/log_format.js rename to packages/kbn-legacy-logging/src/log_format.ts index 6edda8c4be907..e357c2420c178 100644 --- a/src/legacy/server/logging/log_format.js +++ b/packages/kbn-legacy-logging/src/log_format.ts @@ -19,16 +19,29 @@ import Stream from 'stream'; import moment from 'moment-timezone'; -import { get, _ } from 'lodash'; +import _ from 'lodash'; import queryString from 'query-string'; import numeral from '@elastic/numeral'; import chalk from 'chalk'; +// @ts-expect-error missing type def import stringify from 'json-stringify-safe'; -import applyFiltersToKeys from './apply_filters_to_keys'; import { inspect } from 'util'; -import { logWithMetadata } from './log_with_metadata'; -function serializeError(err = {}) { +import { applyFiltersToKeys } from './utils'; +import { getLogEventData } from './metadata'; +import { LegacyLoggingConfig } from './schema'; +import { + AnyEvent, + isResponseEvent, + isOpsEvent, + isErrorEvent, + isLogEvent, + isUndeclaredErrorEvent, +} from './log_events'; + +export type LogFormatConfig = Pick; + +function serializeError(err: any = {}) { return { message: err.message, name: err.name, @@ -38,34 +51,37 @@ function serializeError(err = {}) { }; } -const levelColor = function (code) { - if (code < 299) return chalk.green(code); - if (code < 399) return chalk.yellow(code); - if (code < 499) return chalk.magentaBright(code); - return chalk.red(code); +const levelColor = function (code: number) { + if (code < 299) return chalk.green(String(code)); + if (code < 399) return chalk.yellow(String(code)); + if (code < 499) return chalk.magentaBright(String(code)); + return chalk.red(String(code)); }; -export default class TransformObjStream extends Stream.Transform { - constructor(config) { +export abstract class BaseLogFormat extends Stream.Transform { + constructor(private readonly config: LogFormatConfig) { super({ readableObjectMode: false, writableObjectMode: true, }); - this.config = config; } - filter(data) { - if (!this.config.filter) return data; + abstract format(data: Record): string; + + filter(data: Record) { + if (!this.config.filter) { + return data; + } return applyFiltersToKeys(data, this.config.filter); } - _transform(event, enc, next) { + _transform(event: AnyEvent, enc: string, next: Stream.TransformCallback) { const data = this.filter(this.readEvent(event)); this.push(this.format(data) + '\n'); next(); } - extractAndFormatTimestamp(data, format) { + extractAndFormatTimestamp(data: Record, format?: string) { const { timezone } = this.config; const date = moment(data['@timestamp']); if (timezone) { @@ -74,18 +90,18 @@ export default class TransformObjStream extends Stream.Transform { return date.format(format); } - readEvent(event) { - const data = { + readEvent(event: AnyEvent) { + const data: Record = { type: event.event, '@timestamp': event.timestamp, - tags: [].concat(event.tags || []), + tags: [...(event.tags || [])], pid: event.pid, }; - if (data.type === 'response') { + if (isResponseEvent(event)) { _.defaults(data, _.pick(event, ['method', 'statusCode'])); - const source = get(event, 'source', {}); + const source = _.get(event, 'source', {}); data.req = { url: event.path, method: event.method || '', @@ -95,21 +111,21 @@ export default class TransformObjStream extends Stream.Transform { referer: source.referer, }; - let contentLength = 0; - if (typeof event.responsePayload === 'object') { - contentLength = stringify(event.responsePayload).length; - } else { - contentLength = String(event.responsePayload).length; - } + const contentLength = + event.responsePayload === 'object' + ? stringify(event.responsePayload).length + : String(event.responsePayload).length; data.res = { statusCode: event.statusCode, responseTime: event.responseTime, - contentLength: contentLength, + contentLength, }; const query = queryString.stringify(event.query, { sort: false }); - if (query) data.req.url += '?' + query; + if (query) { + data.req.url += '?' + query; + } data.message = data.req.method.toUpperCase() + ' '; data.message += data.req.url; @@ -118,38 +134,38 @@ export default class TransformObjStream extends Stream.Transform { data.message += ' '; data.message += chalk.gray(data.res.responseTime + 'ms'); data.message += chalk.gray(' - ' + numeral(contentLength).format('0.0b')); - } else if (data.type === 'ops') { + } else if (isOpsEvent(event)) { _.defaults(data, _.pick(event, ['pid', 'os', 'proc', 'load'])); data.message = chalk.gray('memory: '); - data.message += numeral(get(data, 'proc.mem.heapUsed')).format('0.0b'); + data.message += numeral(_.get(data, 'proc.mem.heapUsed')).format('0.0b'); data.message += ' '; data.message += chalk.gray('uptime: '); - data.message += numeral(get(data, 'proc.uptime')).format('00:00:00'); + data.message += numeral(_.get(data, 'proc.uptime')).format('00:00:00'); data.message += ' '; data.message += chalk.gray('load: ['); - data.message += get(data, 'os.load', []) - .map(function (val) { + data.message += _.get(data, 'os.load', []) + .map((val: number) => { return numeral(val).format('0.00'); }) .join(' '); data.message += chalk.gray(']'); data.message += ' '; data.message += chalk.gray('delay: '); - data.message += numeral(get(data, 'proc.delay')).format('0.000'); - } else if (data.type === 'error') { + data.message += numeral(_.get(data, 'proc.delay')).format('0.000'); + } else if (isErrorEvent(event)) { data.level = 'error'; data.error = serializeError(event.error); data.url = event.url; - const message = get(event, 'error.message'); + const message = _.get(event, 'error.message'); data.message = message || 'Unknown error (no message)'; - } else if (event.error instanceof Error) { + } else if (isUndeclaredErrorEvent(event)) { data.type = 'error'; data.level = _.includes(event.tags, 'fatal') ? 'fatal' : 'error'; data.error = serializeError(event.error); - const message = get(event, 'error.message'); + const message = _.get(event, 'error.message'); data.message = message || 'Unknown error object (no message)'; - } else if (logWithMetadata.isLogEvent(event.data)) { - _.assign(data, logWithMetadata.getLogEventData(event.data)); + } else if (isLogEvent(event)) { + _.assign(data, getLogEventData(event.data)); } else { data.message = _.isString(event.data) ? event.data : inspect(event.data); } diff --git a/src/legacy/server/logging/log_format_json.test.js b/packages/kbn-legacy-logging/src/log_format_json.test.ts similarity index 79% rename from src/legacy/server/logging/log_format_json.test.js rename to packages/kbn-legacy-logging/src/log_format_json.test.ts index ec7296d21672b..f762daf01e5fa 100644 --- a/src/legacy/server/logging/log_format_json.test.js +++ b/packages/kbn-legacy-logging/src/log_format_json.test.ts @@ -19,30 +19,31 @@ import moment from 'moment'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { attachMetaData } from '../../../../src/core/server/legacy/logging/legacy_logging_server'; -import { createListStream, createPromiseFromStreams } from '../../../core/server/utils'; - -import KbnLoggerJsonFormat from './log_format_json'; +import { attachMetaData } from './metadata'; +import { createListStream, createPromiseFromStreams } from './test_utils'; +import { KbnLoggerJsonFormat } from './log_format_json'; const time = +moment('2010-01-01T05:15:59Z', moment.ISO_8601); -const makeEvent = (eventType) => ({ +const makeEvent = (eventType: string) => ({ event: eventType, timestamp: time, }); describe('KbnLoggerJsonFormat', () => { - const config = {}; + const config: any = {}; describe('event types and messages', () => { - let format; + let format: KbnLoggerJsonFormat; beforeEach(() => { format = new KbnLoggerJsonFormat(config); }); it('log', async () => { - const result = await createPromiseFromStreams([createListStream([makeEvent('log')]), format]); + const result = await createPromiseFromStreams([ + createListStream([makeEvent('log')]), + format, + ]); const { type, message } = JSON.parse(result); expect(type).toBe('log'); @@ -64,7 +65,7 @@ describe('KbnLoggerJsonFormat', () => { referer: 'elastic.co', }, }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { type, method, statusCode, message, req } = JSON.parse(result); expect(type).toBe('response'); @@ -82,7 +83,7 @@ describe('KbnLoggerJsonFormat', () => { load: [1, 1, 2], }, }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { type, message } = JSON.parse(result); expect(type).toBe('ops'); @@ -98,7 +99,7 @@ describe('KbnLoggerJsonFormat', () => { }), tags: ['tag1', 'tag2'], }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, prop1, prop2, tags } = JSON.parse(result); expect(level).toBe(undefined); @@ -117,7 +118,7 @@ describe('KbnLoggerJsonFormat', () => { }), tags: ['tag1', 'tag2'], }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, prop1, prop2, tags } = JSON.parse(result); expect(level).toBe(undefined); @@ -132,7 +133,7 @@ describe('KbnLoggerJsonFormat', () => { data: attachMetaData('message for event'), tags: ['tag1', 'tag2'], }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, prop1, prop2, tags } = JSON.parse(result); expect(level).toBe(undefined); @@ -151,7 +152,7 @@ describe('KbnLoggerJsonFormat', () => { }), tags: ['tag1', 'tag2'], }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, prop1, prop2, tags } = JSON.parse(result); expect(level).toBe('error'); @@ -170,7 +171,7 @@ describe('KbnLoggerJsonFormat', () => { message: 'test error 0', }, }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, error } = JSON.parse(result); expect(level).toBe('error'); @@ -183,7 +184,7 @@ describe('KbnLoggerJsonFormat', () => { event: 'error', error: {}, }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, error } = JSON.parse(result); expect(level).toBe('error'); @@ -193,9 +194,9 @@ describe('KbnLoggerJsonFormat', () => { it('event error instanceof Error', async () => { const event = { - error: new Error('test error 2'), + error: new Error('test error 2') as any, }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, error } = JSON.parse(result); expect(level).toBe('error'); @@ -210,10 +211,10 @@ describe('KbnLoggerJsonFormat', () => { it('event error instanceof Error - fatal', async () => { const event = { - error: new Error('test error 2'), + error: new Error('test error 2') as any, tags: ['fatal', 'tag2'], }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { tags, level, message, error } = JSON.parse(result); expect(tags).toEqual(['fatal', 'tag2']); @@ -229,9 +230,9 @@ describe('KbnLoggerJsonFormat', () => { it('event error instanceof Error, no message', async () => { const event = { - error: new Error(''), + error: new Error('') as any, }; - const result = await createPromiseFromStreams([createListStream([event]), format]); + const result = await createPromiseFromStreams([createListStream([event]), format]); const { level, message, error } = JSON.parse(result); expect(level).toBe('error'); @@ -250,18 +251,24 @@ describe('KbnLoggerJsonFormat', () => { it('logs in UTC', async () => { const format = new KbnLoggerJsonFormat({ timezone: 'UTC', - }); + } as any); - const result = await createPromiseFromStreams([createListStream([makeEvent('log')]), format]); + const result = await createPromiseFromStreams([ + createListStream([makeEvent('log')]), + format, + ]); const { '@timestamp': timestamp } = JSON.parse(result); expect(timestamp).toBe(moment.utc(time).format()); }); it('logs in local timezone timezone is undefined', async () => { - const format = new KbnLoggerJsonFormat({}); + const format = new KbnLoggerJsonFormat({} as any); - const result = await createPromiseFromStreams([createListStream([makeEvent('log')]), format]); + const result = await createPromiseFromStreams([ + createListStream([makeEvent('log')]), + format, + ]); const { '@timestamp': timestamp } = JSON.parse(result); expect(timestamp).toBe(moment(time).format()); diff --git a/src/legacy/server/logging/log_format_json.js b/packages/kbn-legacy-logging/src/log_format_json.ts similarity index 82% rename from src/legacy/server/logging/log_format_json.js rename to packages/kbn-legacy-logging/src/log_format_json.ts index bfceb78b24504..7961fda7912cc 100644 --- a/src/legacy/server/logging/log_format_json.js +++ b/packages/kbn-legacy-logging/src/log_format_json.ts @@ -17,15 +17,16 @@ * under the License. */ -import LogFormat from './log_format'; +// @ts-expect-error missing type def import stringify from 'json-stringify-safe'; +import { BaseLogFormat } from './log_format'; -const stripColors = function (string) { +const stripColors = function (string: string) { return string.replace(/\u001b[^m]+m/g, ''); }; -export default class KbnLoggerJsonFormat extends LogFormat { - format(data) { +export class KbnLoggerJsonFormat extends BaseLogFormat { + format(data: Record) { data.message = stripColors(data.message); data['@timestamp'] = this.extractAndFormatTimestamp(data); return stringify(data); diff --git a/src/legacy/server/logging/log_format_string.test.js b/packages/kbn-legacy-logging/src/log_format_string.test.ts similarity index 84% rename from src/legacy/server/logging/log_format_string.test.js rename to packages/kbn-legacy-logging/src/log_format_string.test.ts index 842325865cce2..0ed233228c1fd 100644 --- a/src/legacy/server/logging/log_format_string.test.js +++ b/packages/kbn-legacy-logging/src/log_format_string.test.ts @@ -18,12 +18,10 @@ */ import moment from 'moment'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { attachMetaData } from '../../../../src/core/server/legacy/logging/legacy_logging_server'; -import { createListStream, createPromiseFromStreams } from '../../../core/server/utils'; - -import KbnLoggerStringFormat from './log_format_string'; +import { attachMetaData } from './metadata'; +import { createListStream, createPromiseFromStreams } from './test_utils'; +import { KbnLoggerStringFormat } from './log_format_string'; const time = +moment('2010-01-01T05:15:59Z', moment.ISO_8601); @@ -39,7 +37,7 @@ describe('KbnLoggerStringFormat', () => { it('logs in UTC', async () => { const format = new KbnLoggerStringFormat({ timezone: 'UTC', - }); + } as any); const result = await createPromiseFromStreams([createListStream([makeEvent()]), format]); @@ -47,7 +45,7 @@ describe('KbnLoggerStringFormat', () => { }); it('logs in local timezone when timezone is undefined', async () => { - const format = new KbnLoggerStringFormat({}); + const format = new KbnLoggerStringFormat({} as any); const result = await createPromiseFromStreams([createListStream([makeEvent()]), format]); @@ -55,7 +53,7 @@ describe('KbnLoggerStringFormat', () => { }); describe('with metadata', () => { it('does not log meta data', async () => { - const format = new KbnLoggerStringFormat({}); + const format = new KbnLoggerStringFormat({} as any); const event = { data: attachMetaData('message for event', { prop1: 'value1', diff --git a/src/legacy/server/logging/log_format_string.js b/packages/kbn-legacy-logging/src/log_format_string.ts similarity index 84% rename from src/legacy/server/logging/log_format_string.js rename to packages/kbn-legacy-logging/src/log_format_string.ts index cbbf71dd894ac..3f024fac55119 100644 --- a/src/legacy/server/logging/log_format_string.js +++ b/packages/kbn-legacy-logging/src/log_format_string.ts @@ -20,11 +20,11 @@ import _ from 'lodash'; import chalk from 'chalk'; -import LogFormat from './log_format'; +import { BaseLogFormat } from './log_format'; const statuses = ['err', 'info', 'error', 'warning', 'fatal', 'status', 'debug']; -const typeColors = { +const typeColors: Record = { log: 'white', req: 'green', res: 'green', @@ -45,18 +45,19 @@ const typeColors = { scss: 'magentaBright', }; -const color = _.memoize(function (name) { +const color = _.memoize((name: string): ((...text: string[]) => string) => { + // @ts-expect-error couldn't even get rid of the error with an any cast return chalk[typeColors[name]] || _.identity; }); -const type = _.memoize(function (t) { +const type = _.memoize((t: string) => { return color(t)(_.pad(t, 7).slice(0, 7)); }); const workerType = process.env.kbnWorkerType ? `${type(process.env.kbnWorkerType)} ` : ''; -export default class KbnLoggerStringFormat extends LogFormat { - format(data) { +export class KbnLoggerStringFormat extends BaseLogFormat { + format(data: Record) { const time = color('time')(this.extractAndFormatTimestamp(data, 'HH:mm:ss.SSS')); const msg = data.error ? color('error')(data.error.stack) : color('message')(data.message); diff --git a/src/legacy/server/logging/log_interceptor.test.js b/packages/kbn-legacy-logging/src/log_interceptor.test.ts similarity index 90% rename from src/legacy/server/logging/log_interceptor.test.js rename to packages/kbn-legacy-logging/src/log_interceptor.test.ts index 492d1ffc8d167..32da6432cc443 100644 --- a/src/legacy/server/logging/log_interceptor.test.js +++ b/packages/kbn-legacy-logging/src/log_interceptor.test.ts @@ -17,13 +17,15 @@ * under the License. */ +import { ErrorEvent } from './log_events'; import { LogInterceptor } from './log_interceptor'; -function stubClientErrorEvent(errorMeta) { +function stubClientErrorEvent(errorMeta: Record): ErrorEvent { const error = new Error(); Object.assign(error, errorMeta); return { event: 'error', + url: '', pid: 1234, timestamp: Date.now(), tags: ['connection', 'client', 'error'], @@ -35,7 +37,7 @@ const stubEconnresetEvent = () => stubClientErrorEvent({ code: 'ECONNRESET' }); const stubEpipeEvent = () => stubClientErrorEvent({ errno: 'EPIPE' }); const stubEcanceledEvent = () => stubClientErrorEvent({ errno: 'ECANCELED' }); -function assertDowngraded(transformed) { +function assertDowngraded(transformed: Record) { expect(!!transformed).toBe(true); expect(transformed).toHaveProperty('event', 'log'); expect(transformed).toHaveProperty('tags'); @@ -47,13 +49,13 @@ describe('server logging LogInterceptor', () => { it('transforms ECONNRESET events', () => { const interceptor = new LogInterceptor(); const event = stubEconnresetEvent(); - assertDowngraded(interceptor.downgradeIfEconnreset(event)); + assertDowngraded(interceptor.downgradeIfEconnreset(event)!); }); it('does not match if the tags are not in order', () => { const interceptor = new LogInterceptor(); const event = stubEconnresetEvent(); - event.tags = [...event.tags.slice(1), event.tags[0]]; + event.tags = [...event.tags!.slice(1), event.tags![0]]; expect(interceptor.downgradeIfEconnreset(event)).toBe(null); }); @@ -75,13 +77,13 @@ describe('server logging LogInterceptor', () => { it('transforms EPIPE events', () => { const interceptor = new LogInterceptor(); const event = stubEpipeEvent(); - assertDowngraded(interceptor.downgradeIfEpipe(event)); + assertDowngraded(interceptor.downgradeIfEpipe(event)!); }); it('does not match if the tags are not in order', () => { const interceptor = new LogInterceptor(); const event = stubEpipeEvent(); - event.tags = [...event.tags.slice(1), event.tags[0]]; + event.tags = [...event.tags!.slice(1), event.tags![0]]; expect(interceptor.downgradeIfEpipe(event)).toBe(null); }); @@ -103,13 +105,13 @@ describe('server logging LogInterceptor', () => { it('transforms ECANCELED events', () => { const interceptor = new LogInterceptor(); const event = stubEcanceledEvent(); - assertDowngraded(interceptor.downgradeIfEcanceled(event)); + assertDowngraded(interceptor.downgradeIfEcanceled(event)!); }); it('does not match if the tags are not in order', () => { const interceptor = new LogInterceptor(); const event = stubEcanceledEvent(); - event.tags = [...event.tags.slice(1), event.tags[0]]; + event.tags = [...event.tags!.slice(1), event.tags![0]]; expect(interceptor.downgradeIfEcanceled(event)).toBe(null); }); @@ -131,7 +133,7 @@ describe('server logging LogInterceptor', () => { it('transforms https requests when serving http errors', () => { const interceptor = new LogInterceptor(); const event = stubClientErrorEvent({ message: 'Parse Error', code: 'HPE_INVALID_METHOD' }); - assertDowngraded(interceptor.downgradeIfHTTPSWhenHTTP(event)); + assertDowngraded(interceptor.downgradeIfHTTPSWhenHTTP(event)!); }); it('ignores non events', () => { @@ -150,7 +152,7 @@ describe('server logging LogInterceptor', () => { '4584650176:error:1408F09C:SSL routines:ssl3_get_record:http request:../deps/openssl/openssl/ssl/record/ssl3_record.c:322:\n'; const interceptor = new LogInterceptor(); const event = stubClientErrorEvent({ message }); - assertDowngraded(interceptor.downgradeIfHTTPWhenHTTPS(event)); + assertDowngraded(interceptor.downgradeIfHTTPWhenHTTPS(event)!); }); it('ignores non events', () => { diff --git a/src/legacy/server/logging/log_interceptor.js b/packages/kbn-legacy-logging/src/log_interceptor.ts similarity index 81% rename from src/legacy/server/logging/log_interceptor.js rename to packages/kbn-legacy-logging/src/log_interceptor.ts index 2298d83aa2857..2d559dc1ef55c 100644 --- a/src/legacy/server/logging/log_interceptor.js +++ b/packages/kbn-legacy-logging/src/log_interceptor.ts @@ -19,6 +19,7 @@ import Stream from 'stream'; import { get, isEqual } from 'lodash'; +import { AnyEvent } from './log_events'; /** * Matches error messages when clients connect via HTTP instead of HTTPS; see unit test for full message. Warning: this can change when Node @@ -26,25 +27,32 @@ import { get, isEqual } from 'lodash'; */ const OPENSSL_GET_RECORD_REGEX = /ssl3_get_record:http/; -function doTagsMatch(event, tags) { - return isEqual(get(event, 'tags'), tags); +function doTagsMatch(event: AnyEvent, tags: string[]) { + return isEqual(event.tags, tags); } -function doesMessageMatch(errorMessage, match) { - if (!errorMessage) return false; - const isRegExp = match instanceof RegExp; - if (isRegExp) return match.test(errorMessage); +function doesMessageMatch(errorMessage: string, match: RegExp | string) { + if (!errorMessage) { + return false; + } + if (match instanceof RegExp) { + return match.test(errorMessage); + } return errorMessage === match; } // converts the given event into a debug log if it's an error of the given type -function downgradeIfErrorType(errorType, event) { +function downgradeIfErrorType(errorType: string, event: AnyEvent) { const isClientError = doTagsMatch(event, ['connection', 'client', 'error']); - if (!isClientError) return null; + if (!isClientError) { + return null; + } const matchesErrorType = get(event, 'error.code') === errorType || get(event, 'error.errno') === errorType; - if (!matchesErrorType) return null; + if (!matchesErrorType) { + return null; + } const errorTypeTag = errorType.toLowerCase(); @@ -57,12 +65,14 @@ function downgradeIfErrorType(errorType, event) { }; } -function downgradeIfErrorMessage(match, event) { +function downgradeIfErrorMessage(match: RegExp | string, event: AnyEvent) { const isClientError = doTagsMatch(event, ['connection', 'client', 'error']); const errorMessage = get(event, 'error.message'); const matchesErrorMessage = isClientError && doesMessageMatch(errorMessage, match); - if (!matchesErrorMessage) return null; + if (!matchesErrorMessage) { + return null; + } return { event: 'log', @@ -91,7 +101,7 @@ export class LogInterceptor extends Stream.Transform { * * @param {object} - log event */ - downgradeIfEconnreset(event) { + downgradeIfEconnreset(event: AnyEvent) { return downgradeIfErrorType('ECONNRESET', event); } @@ -105,7 +115,7 @@ export class LogInterceptor extends Stream.Transform { * * @param {object} - log event */ - downgradeIfEpipe(event) { + downgradeIfEpipe(event: AnyEvent) { return downgradeIfErrorType('EPIPE', event); } @@ -119,19 +129,19 @@ export class LogInterceptor extends Stream.Transform { * * @param {object} - log event */ - downgradeIfEcanceled(event) { + downgradeIfEcanceled(event: AnyEvent) { return downgradeIfErrorType('ECANCELED', event); } - downgradeIfHTTPSWhenHTTP(event) { + downgradeIfHTTPSWhenHTTP(event: AnyEvent) { return downgradeIfErrorType('HPE_INVALID_METHOD', event); } - downgradeIfHTTPWhenHTTPS(event) { + downgradeIfHTTPWhenHTTPS(event: AnyEvent) { return downgradeIfErrorMessage(OPENSSL_GET_RECORD_REGEX, event); } - _transform(event, enc, next) { + _transform(event: AnyEvent, enc: string, next: Stream.TransformCallback) { const downgraded = this.downgradeIfEconnreset(event) || this.downgradeIfEpipe(event) || diff --git a/src/legacy/server/logging/log_reporter.js b/packages/kbn-legacy-logging/src/log_reporter.ts similarity index 64% rename from src/legacy/server/logging/log_reporter.js rename to packages/kbn-legacy-logging/src/log_reporter.ts index 4afb00b568844..8ecaf348bac04 100644 --- a/src/legacy/server/logging/log_reporter.js +++ b/packages/kbn-legacy-logging/src/log_reporter.ts @@ -17,27 +17,21 @@ * under the License. */ +// @ts-expect-error missing type def import { Squeeze } from '@hapi/good-squeeze'; -import { createWriteStream as writeStr } from 'fs'; +import { createWriteStream as writeStr, WriteStream } from 'fs'; -import LogFormatJson from './log_format_json'; -import LogFormatString from './log_format_string'; +import { KbnLoggerJsonFormat } from './log_format_json'; +import { KbnLoggerStringFormat } from './log_format_string'; import { LogInterceptor } from './log_interceptor'; +import { LogFormatConfig } from './log_format'; -// NOTE: legacy logger creates a new stream for each new access -// In https://github.com/elastic/kibana/pull/55937 we reach the max listeners -// default limit of 10 for process.stdout which starts a long warning/error -// thrown every time we start the server. -// In order to keep using the legacy logger until we remove it I'm just adding -// a new hard limit here. -process.stdout.setMaxListeners(25); - -export function getLoggerStream({ events, config }) { +export function getLogReporter({ events, config }: { events: any; config: LogFormatConfig }) { const squeeze = new Squeeze(events); - const format = config.json ? new LogFormatJson(config) : new LogFormatString(config); + const format = config.json ? new KbnLoggerJsonFormat(config) : new KbnLoggerStringFormat(config); const logInterceptor = new LogInterceptor(); - let dest; + let dest: WriteStream | NodeJS.WritableStream; if (config.dest === 'stdout') { dest = process.stdout; } else { diff --git a/src/legacy/server/logging/log_with_metadata.js b/packages/kbn-legacy-logging/src/metadata.ts similarity index 55% rename from src/legacy/server/logging/log_with_metadata.js rename to packages/kbn-legacy-logging/src/metadata.ts index 73e03a154907a..8b7c2f8f87c59 100644 --- a/src/legacy/server/logging/log_with_metadata.js +++ b/packages/kbn-legacy-logging/src/metadata.ts @@ -16,30 +16,38 @@ * specific language governing permissions and limitations * under the License. */ + import { isPlainObject } from 'lodash'; -import { - metadataSymbol, - attachMetaData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../src/core/server/legacy/logging/legacy_logging_server'; +export const metadataSymbol = Symbol('log message with metadata'); -export const logWithMetadata = { - isLogEvent(eventData) { - return Boolean(isPlainObject(eventData) && eventData[metadataSymbol]); - }, +export interface EventData { + [metadataSymbol]?: EventMetadata; + [key: string]: any; +} - getLogEventData(eventData) { - const { message, metadata } = eventData[metadataSymbol]; - return { - ...metadata, - message, - }; - }, +export interface EventMetadata { + message: string; + metadata: Record; +} + +export const isEventData = (eventData: EventData) => { + return Boolean(isPlainObject(eventData) && eventData[metadataSymbol]); +}; - decorateServer(server) { - server.decorate('server', 'logWithMetadata', (tags, message, metadata = {}) => { - server.log(tags, attachMetaData(message, metadata)); - }); - }, +export const getLogEventData = (eventData: EventData) => { + const { message, metadata } = eventData[metadataSymbol]!; + return { + ...metadata, + message, + }; +}; + +export const attachMetaData = (message: string, metadata: Record = {}) => { + return { + [metadataSymbol]: { + message, + metadata, + }, + }; }; diff --git a/src/legacy/server/logging/rotate/index.ts b/packages/kbn-legacy-logging/src/rotate/index.ts similarity index 92% rename from src/legacy/server/logging/rotate/index.ts rename to packages/kbn-legacy-logging/src/rotate/index.ts index d6b7cfa76f9ee..2387fc530e58b 100644 --- a/src/legacy/server/logging/rotate/index.ts +++ b/packages/kbn-legacy-logging/src/rotate/index.ts @@ -20,13 +20,13 @@ import { isMaster, isWorker } from 'cluster'; import { Server } from '@hapi/hapi'; import { LogRotator } from './log_rotator'; -import { KibanaConfig } from '../../kbn_server'; +import { LegacyLoggingConfig } from '../schema'; let logRotator: LogRotator; -export async function setupLoggingRotate(server: Server, config: KibanaConfig) { +export async function setupLoggingRotate(server: Server, config: LegacyLoggingConfig) { // If log rotate is not enabled we skip - if (!config.get('logging.rotate.enabled')) { + if (!config.rotate.enabled) { return; } @@ -38,7 +38,7 @@ export async function setupLoggingRotate(server: Server, config: KibanaConfig) { // We don't want to run logging rotate server if // we are not logging to a file - if (config.get('logging.dest') === 'stdout') { + if (config.dest === 'stdout') { server.log( ['warning', 'logging:rotate'], 'Log rotation is enabled but logging.dest is configured for stdout. Set logging.dest to a file for this setting to take effect.' diff --git a/src/legacy/server/logging/rotate/log_rotator.test.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts similarity index 93% rename from src/legacy/server/logging/rotate/log_rotator.test.ts rename to packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts index 8f67b47f6949e..1f6407d2cca30 100644 --- a/src/legacy/server/logging/rotate/log_rotator.test.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts @@ -19,10 +19,10 @@ import del from 'del'; import fs, { existsSync, mkdirSync, statSync, writeFileSync } from 'fs'; -import { LogRotator } from './log_rotator'; import { tmpdir } from 'os'; import { dirname, join } from 'path'; -import lodash from 'lodash'; +import { LogRotator } from './log_rotator'; +import { LegacyLoggingConfig } from '../schema'; const mockOn = jest.fn(); jest.mock('chokidar', () => ({ @@ -32,19 +32,26 @@ jest.mock('chokidar', () => ({ })), })); -lodash.throttle = (fn: any) => fn; +jest.mock('lodash', () => ({ + ...(jest.requireActual('lodash') as any), + throttle: (fn: any) => fn, +})); const tempDir = join(tmpdir(), 'kbn_log_rotator_test'); const testFilePath = join(tempDir, 'log_rotator_test_log_file.log'); -const createLogRotatorConfig: any = (logFilePath: string) => { - return new Map([ - ['logging.dest', logFilePath], - ['logging.rotate.everyBytes', 2], - ['logging.rotate.keepFiles', 2], - ['logging.rotate.usePolling', false], - ['logging.rotate.pollingInterval', 10000], - ] as any); +const createLogRotatorConfig = (logFilePath: string): LegacyLoggingConfig => { + return { + dest: logFilePath, + rotate: { + enabled: true, + keepFiles: 2, + everyBytes: 2, + usePolling: false, + pollingInterval: 10000, + pollingPolicyTestTimeout: 4000, + }, + } as LegacyLoggingConfig; }; const mockServer: any = { @@ -62,7 +69,7 @@ describe('LogRotator', () => { }); afterEach(() => { - del.sync(dirname(testFilePath), { force: true }); + del.sync(tempDir, { force: true }); mockOn.mockClear(); }); @@ -71,14 +78,14 @@ describe('LogRotator', () => { const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); + await logRotator.start(); expect(logRotator.running).toBe(true); await logRotator.stop(); - const testLogFileDir = dirname(testFilePath); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); + expect(existsSync(join(tempDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); }); it('rotates log file when equal than set limit over time', async () => { diff --git a/src/legacy/server/logging/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts similarity index 94% rename from src/legacy/server/logging/rotate/log_rotator.ts rename to packages/kbn-legacy-logging/src/rotate/log_rotator.ts index c4054b2daed45..54181e30d6007 100644 --- a/src/legacy/server/logging/rotate/log_rotator.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts @@ -27,7 +27,7 @@ import { basename, dirname, join, sep } from 'path'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { promisify } from 'util'; -import { KibanaConfig } from '../../kbn_server'; +import { LegacyLoggingConfig } from '../schema'; const mkdirAsync = promisify(fs.mkdir); const readdirAsync = promisify(fs.readdir); @@ -37,7 +37,7 @@ const unlinkAsync = promisify(fs.unlink); const writeFileAsync = promisify(fs.writeFile); export class LogRotator { - private readonly config: KibanaConfig; + private readonly config: LegacyLoggingConfig; private readonly log: Server['log']; public logFilePath: string; public everyBytes: number; @@ -52,19 +52,19 @@ export class LogRotator { private stalkerUsePollingPolicyTestTimeout: NodeJS.Timeout | null; public shouldUsePolling: boolean; - constructor(config: KibanaConfig, server: Server) { + constructor(config: LegacyLoggingConfig, server: Server) { this.config = config; this.log = server.log.bind(server); - this.logFilePath = config.get('logging.dest'); - this.everyBytes = config.get('logging.rotate.everyBytes'); - this.keepFiles = config.get('logging.rotate.keepFiles'); + this.logFilePath = config.dest; + this.everyBytes = config.rotate.everyBytes; + this.keepFiles = config.rotate.keepFiles; this.running = false; this.logFileSize = 0; this.isRotating = false; this.throttledRotate = throttle(async () => await this._rotate(), 5000); this.stalker = null; - this.usePolling = config.get('logging.rotate.usePolling'); - this.pollingInterval = config.get('logging.rotate.pollingInterval'); + this.usePolling = config.rotate.usePolling; + this.pollingInterval = config.rotate.pollingInterval; this.shouldUsePolling = false; this.stalkerUsePollingPolicyTestTimeout = null; } @@ -128,7 +128,10 @@ export class LogRotator { }; // setup conditions that would fire the observable - this.stalkerUsePollingPolicyTestTimeout = setTimeout(() => completeFn(true), 15000); + this.stalkerUsePollingPolicyTestTimeout = setTimeout( + () => completeFn(true), + this.config.rotate.pollingPolicyTestTimeout || 15000 + ); testWatcher.on('change', () => completeFn(false)); testWatcher.on('error', () => completeFn(true)); @@ -152,7 +155,7 @@ export class LogRotator { } async _startLogFileSizeMonitor() { - this.usePolling = this.config.get('logging.rotate.usePolling'); + this.usePolling = this.config.rotate.usePolling; this.shouldUsePolling = await this._shouldUsePolling(); if (this.usePolling && !this.shouldUsePolling) { diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts new file mode 100644 index 0000000000000..5f0e4fe89422b --- /dev/null +++ b/packages/kbn-legacy-logging/src/schema.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Joi from 'joi'; + +const HANDLED_IN_KIBANA_PLATFORM = Joi.any().description( + 'This key is handled in the new platform ONLY' +); + +export interface LegacyLoggingConfig { + silent: boolean; + quiet: boolean; + verbose: boolean; + events: Record; + dest: string; + filter: Record; + json: boolean; + timezone?: string; + rotate: { + enabled: boolean; + everyBytes: number; + keepFiles: number; + pollingInterval: number; + usePolling: boolean; + pollingPolicyTestTimeout?: number; + }; +} + +export const legacyLoggingConfigSchema = Joi.object() + .keys({ + appenders: HANDLED_IN_KIBANA_PLATFORM, + loggers: HANDLED_IN_KIBANA_PLATFORM, + root: HANDLED_IN_KIBANA_PLATFORM, + + silent: Joi.boolean().default(false), + + quiet: Joi.boolean().when('silent', { + is: true, + then: Joi.boolean().default(true).valid(true), + otherwise: Joi.boolean().default(false), + }), + + verbose: Joi.boolean().when('quiet', { + is: true, + then: Joi.valid(false).default(false), + otherwise: Joi.boolean().default(false), + }), + events: Joi.any().default({}), + dest: Joi.string().default('stdout'), + filter: Joi.any().default({}), + json: Joi.boolean().when('dest', { + is: 'stdout', + then: Joi.boolean().default(!process.stdout.isTTY), + otherwise: Joi.boolean().default(true), + }), + timezone: Joi.string(), + rotate: Joi.object() + .keys({ + enabled: Joi.boolean().default(false), + everyBytes: Joi.number() + // > 1MB + .greater(1048576) + // < 1GB + .less(1073741825) + // 10MB + .default(10485760), + keepFiles: Joi.number().greater(2).less(1024).default(7), + pollingInterval: Joi.number().greater(5000).less(3600000).default(10000), + usePolling: Joi.boolean().default(false), + }) + .default(), + }) + .default(); diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts new file mode 100644 index 0000000000000..103e81249a136 --- /dev/null +++ b/packages/kbn-legacy-logging/src/setup_logging.ts @@ -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. + */ + +// @ts-expect-error missing typedef +import good from '@elastic/good'; +import { Server } from '@hapi/hapi'; +import { LegacyLoggingConfig } from './schema'; +import { getLoggingConfiguration } from './get_logging_config'; + +export async function setupLogging( + server: Server, + config: LegacyLoggingConfig, + opsInterval: number +) { + // NOTE: legacy logger creates a new stream for each new access + // In https://github.com/elastic/kibana/pull/55937 we reach the max listeners + // default limit of 10 for process.stdout which starts a long warning/error + // thrown every time we start the server. + // In order to keep using the legacy logger until we remove it I'm just adding + // a new hard limit here. + process.stdout.setMaxListeners(25); + + return await server.register({ + plugin: good, + options: getLoggingConfiguration(config, opsInterval), + }); +} + +export function reconfigureLogging( + server: Server, + config: LegacyLoggingConfig, + opsInterval: number +) { + const loggingOptions = getLoggingConfiguration(config, opsInterval); + (server.plugins as any)['@elastic/good'].reconfigure(loggingOptions); +} diff --git a/packages/kbn-legacy-logging/src/test_utils/index.ts b/packages/kbn-legacy-logging/src/test_utils/index.ts new file mode 100644 index 0000000000000..f13c869b563a2 --- /dev/null +++ b/packages/kbn-legacy-logging/src/test_utils/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { createListStream, createPromiseFromStreams } from './streams'; diff --git a/packages/kbn-legacy-logging/src/test_utils/streams.ts b/packages/kbn-legacy-logging/src/test_utils/streams.ts new file mode 100644 index 0000000000000..0f37a13f8a478 --- /dev/null +++ b/packages/kbn-legacy-logging/src/test_utils/streams.ts @@ -0,0 +1,96 @@ +/* + * 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 { pipeline, Writable, Readable } from 'stream'; + +/** + * Create a Readable stream that provides the items + * from a list as objects to subscribers + * + * @param {Array} items - the list of items to provide + * @return {Readable} + */ +export function createListStream(items: T | T[] = []) { + const queue = Array.isArray(items) ? [...items] : [items]; + + return new Readable({ + objectMode: true, + read(size) { + queue.splice(0, size).forEach((item) => { + this.push(item); + }); + + if (!queue.length) { + this.push(null); + } + }, + }); +} + +/** + * Take an array of streams, pipe the output + * from each one into the next, listening for + * errors from any of the streams, and then resolve + * the promise once the final stream has finished + * writing/reading. + * + * If the last stream is readable, it's final value + * will be provided as the promise value. + * + * Errors emitted from any stream will cause + * the promise to be rejected with that error. + * + * @param {Array} streams + * @return {Promise} + */ + +function isReadable(stream: Readable | Writable): stream is Readable { + return 'read' in stream && typeof stream.read === 'function'; +} + +export async function createPromiseFromStreams(streams: [Readable, ...Writable[]]): Promise { + let finalChunk: any; + const last = streams[streams.length - 1]; + if (!isReadable(last) && streams.length === 1) { + // For a nicer error than what stream.pipeline throws + throw new Error('A minimum of 2 streams is required when a non-readable stream is given'); + } + if (isReadable(last)) { + // We are pushing a writable stream to capture the last chunk + streams.push( + new Writable({ + // Use object mode even when "last" stream isn't. This allows to + // capture the last chunk as-is. + objectMode: true, + write(chunk, enc, done) { + finalChunk = chunk; + done(); + }, + }) + ); + } + + return new Promise((resolve, reject) => { + // @ts-expect-error 'pipeline' doesn't support variable length of arguments + pipeline(...streams, (err) => { + if (err) return reject(err); + resolve(finalChunk); + }); + }); +} diff --git a/src/legacy/server/logging/apply_filters_to_keys.test.js b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts similarity index 96% rename from src/legacy/server/logging/apply_filters_to_keys.test.js rename to packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts index e007157e9488b..bfcc7b1c908d4 100644 --- a/src/legacy/server/logging/apply_filters_to_keys.test.js +++ b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import applyFiltersToKeys from './apply_filters_to_keys'; +import { applyFiltersToKeys } from './apply_filters_to_keys'; describe('applyFiltersToKeys(obj, actionsByKey)', function () { it('applies for each key+prop in actionsByKey', function () { diff --git a/src/legacy/server/logging/apply_filters_to_keys.js b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts similarity index 83% rename from src/legacy/server/logging/apply_filters_to_keys.js rename to packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts index 63e5ab4c62f29..8fd7eac57fc32 100644 --- a/src/legacy/server/logging/apply_filters_to_keys.js +++ b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts @@ -17,15 +17,15 @@ * under the License. */ -function toPojo(obj) { +function toPojo(obj: Record) { return JSON.parse(JSON.stringify(obj)); } -function replacer(match, group) { +function replacer(match: string, group: any[]) { return new Array(group.length + 1).join('X'); } -function apply(obj, key, action) { +function apply(obj: Record, key: string, action: string) { for (const k in obj) { if (obj.hasOwnProperty(k)) { let val = obj[k]; @@ -44,14 +44,17 @@ function apply(obj, key, action) { } } } else if (typeof val === 'object') { - val = apply(val, key, action); + val = apply(val as Record, key, action); } } } return obj; } -export default function (obj, actionsByKey) { +export function applyFiltersToKeys( + obj: Record, + actionsByKey: Record +) { return Object.keys(actionsByKey).reduce((output, key) => { return apply(output, key, actionsByKey[key]); }, toPojo(obj)); diff --git a/packages/kbn-legacy-logging/src/utils/index.ts b/packages/kbn-legacy-logging/src/utils/index.ts new file mode 100644 index 0000000000000..5841e7b608284 --- /dev/null +++ b/packages/kbn-legacy-logging/src/utils/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { applyFiltersToKeys } from './apply_filters_to_keys'; diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json new file mode 100644 index 0000000000000..8fd202a2dce8b --- /dev/null +++ b/packages/kbn-legacy-logging/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "types": ["jest", "node"] + }, + "include": ["./src/**/*"] +} diff --git a/packages/kbn-legacy-logging/yarn.lock b/packages/kbn-legacy-logging/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-legacy-logging/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-optimizer/src/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index e918bae86c835..417e38d5fb7ab 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -17,91 +17,67 @@ * under the License. */ -import Path from 'path'; -import Fs from 'fs'; +import { Writable } from 'stream'; -// @ts-expect-error no types available +import chalk from 'chalk'; import * as LmdbStore from 'lmdb-store'; -import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/dev-utils'; - -const LMDB_PKG = JSON.parse( - Fs.readFileSync(Path.resolve(REPO_ROOT, 'node_modules/lmdb-store/package.json'), 'utf8') -); -const CACHE_DIR = Path.resolve( - REPO_ROOT, - `data/node_auto_transpilation_cache/lmdb-${LMDB_PKG.version}/${UPSTREAM_BRANCH}` -); - -const reportError = () => { - // right now I'm not sure we need to worry about errors, the cache isn't actually - // necessary, and if the cache is broken it should just rebuild on the next restart - // of the process. We don't know how often errors occur though and what types of - // things might fail on different machines so we probably want some way to signal - // to users that something is wrong -}; const GLOBAL_ATIME = `${Date.now()}`; const MINUTE = 1000 * 60; const HOUR = MINUTE * 60; const DAY = HOUR * 24; -interface Lmdb { - name: string; - get(key: string): T | undefined; - put(key: string, value: T, version?: number, ifVersion?: number): Promise; - remove(key: string, ifVersion?: number): Promise; - removeSync(key: string): void; - openDB(options: { - name: string; - encoding: 'msgpack' | 'string' | 'json' | 'binary'; - }): Lmdb; - getRange(options?: { - start?: T; - end?: T; - reverse?: boolean; - limit?: number; - versions?: boolean; - }): Iterable<{ key: string; value: T }>; -} +const dbName = (db: LmdbStore.Database) => + // @ts-expect-error db.name is not a documented/typed property + db.name; export class Cache { - private readonly codes: Lmdb; - private readonly atimes: Lmdb; - private readonly mtimes: Lmdb; - private readonly sourceMaps: Lmdb; + private readonly codes: LmdbStore.RootDatabase; + private readonly atimes: LmdbStore.Database; + private readonly mtimes: LmdbStore.Database; + private readonly sourceMaps: LmdbStore.Database; private readonly prefix: string; + private readonly log?: Writable; + private readonly timer: NodeJS.Timer; - constructor(config: { prefix: string }) { + constructor(config: { dir: string; prefix: string; log?: Writable }) { this.prefix = config.prefix; + this.log = config.log; - this.codes = LmdbStore.open({ + this.codes = LmdbStore.open(config.dir, { name: 'codes', - path: CACHE_DIR, + encoding: 'string', maxReaders: 500, }); - this.atimes = this.codes.openDB({ + // TODO: redundant 'name' syntax is necessary because of a bug that I have yet to fix + this.atimes = this.codes.openDB('atimes', { name: 'atimes', encoding: 'string', }); - this.mtimes = this.codes.openDB({ + this.mtimes = this.codes.openDB('mtimes', { name: 'mtimes', encoding: 'string', }); - this.sourceMaps = this.codes.openDB({ + this.sourceMaps = this.codes.openDB('sourceMaps', { name: 'sourceMaps', - encoding: 'msgpack', + encoding: 'string', }); // after the process has been running for 30 minutes prune the // keys which haven't been used in 30 days. We use `unref()` to // make sure this timer doesn't hold other processes open // unexpectedly - setTimeout(() => { + this.timer = setTimeout(() => { this.pruneOldKeys(); - }, 30 * MINUTE).unref(); + }, 30 * MINUTE); + + // timer.unref is not defined in jest which emulates the dom by default + if (typeof this.timer.unref === 'function') { + this.timer.unref(); + } } getMtime(path: string) { @@ -110,45 +86,78 @@ export class Cache { getCode(path: string) { const key = this.getKey(path); + const code = this.safeGet(this.codes, key); - // when we use a file from the cache set the "atime" of that cache entry - // so that we know which cache items we use and which haven't been - // touched in a long time (currently 30 days) - this.atimes.put(key, GLOBAL_ATIME).catch(reportError); + if (code !== undefined) { + // when we use a file from the cache set the "atime" of that cache entry + // so that we know which cache items we use and which haven't been + // touched in a long time (currently 30 days) + this.safePut(this.atimes, key, GLOBAL_ATIME); + } - return this.safeGet(this.codes, key); + return code; } getSourceMap(path: string) { - return this.safeGet(this.sourceMaps, this.getKey(path)); + const map = this.safeGet(this.sourceMaps, this.getKey(path)); + if (typeof map === 'string') { + return JSON.parse(map); + } } - update(path: string, file: { mtime: string; code: string; map: any }) { + async update(path: string, file: { mtime: string; code: string; map: any }) { const key = this.getKey(path); - Promise.all([ - this.atimes.put(key, GLOBAL_ATIME), - this.mtimes.put(key, file.mtime), - this.codes.put(key, file.code), - this.sourceMaps.put(key, file.map), - ]).catch(reportError); + await Promise.all([ + this.safePut(this.atimes, key, GLOBAL_ATIME), + this.safePut(this.mtimes, key, file.mtime), + this.safePut(this.codes, key, file.code), + this.safePut(this.sourceMaps, key, JSON.stringify(file.map)), + ]); + } + + close() { + clearTimeout(this.timer); } private getKey(path: string) { return `${this.prefix}${path}`; } - private safeGet(db: Lmdb, key: string) { + private safeGet(db: LmdbStore.Database, key: string) { try { - return db.get(key); + const value = db.get(key); + this.debug(value === undefined ? 'MISS' : 'HIT', db, key); + return value; } catch (error) { - process.stderr.write( - `failed to read node transpilation [${db.name}] cache for [${key}]: ${error.stack}\n` - ); - db.removeSync(key); + this.logError('GET', db, key, error); } } + private async safePut(db: LmdbStore.Database, key: string, value: V) { + try { + await db.put(key, value); + this.debug('PUT', db, key); + } catch (error) { + this.logError('PUT', db, key, error); + } + } + + private debug(type: string, db: LmdbStore.Database, key: LmdbStore.Key) { + if (this.log) { + this.log.write(`${type} [${dbName(db)}] ${String(key)}\n`); + } + } + + private logError(type: 'GET' | 'PUT', db: LmdbStore.Database, key: LmdbStore.Key, error: Error) { + this.debug(`ERROR/${type}`, db, `${String(key)}: ${error.stack}`); + process.stderr.write( + chalk.red( + `[@kbn/optimizer/node] ${type} error [${dbName(db)}/${String(key)}]: ${error.stack}\n` + ) + ); + } + private async pruneOldKeys() { try { const ATIME_LIMIT = Date.now() - 30 * DAY; @@ -157,9 +166,10 @@ export class Cache { const validKeys: string[] = []; const invalidKeys: string[] = []; + // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 for (const { key, value } of this.atimes.getRange()) { - const atime = parseInt(value, 10); - if (atime < ATIME_LIMIT) { + const atime = parseInt(`${value}`, 10); + if (Number.isNaN(atime) || atime < ATIME_LIMIT) { invalidKeys.push(key); } else { validKeys.push(key); diff --git a/packages/kbn-optimizer/src/node/integration_tests/cache.test.ts b/packages/kbn-optimizer/src/node/integration_tests/cache.test.ts new file mode 100644 index 0000000000000..c860164d4306a --- /dev/null +++ b/packages/kbn-optimizer/src/node/integration_tests/cache.test.ts @@ -0,0 +1,97 @@ +/* + * 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 Path from 'path'; +import { Writable } from 'stream'; + +import del from 'del'; + +import { Cache } from '../cache'; + +const DIR = Path.resolve(__dirname, '../__tmp__/cache'); + +const makeTestLog = () => { + const log = Object.assign( + new Writable({ + write(chunk, enc, cb) { + log.output += chunk; + cb(); + }, + }), + { + output: '', + } + ); + + return log; +}; + +const instances: Cache[] = []; +const makeCache = (...options: ConstructorParameters) => { + const instance = new Cache(...options); + instances.push(instance); + return instance; +}; + +beforeEach(async () => await del(DIR)); +afterEach(async () => { + await del(DIR); + for (const instance of instances) { + instance.close(); + } + instances.length = 0; +}); + +it('returns undefined until values are set', async () => { + const path = '/foo/bar.js'; + const mtime = new Date().toJSON(); + const log = makeTestLog(); + const cache = makeCache({ + dir: DIR, + prefix: 'foo', + log, + }); + + expect(cache.getMtime(path)).toBe(undefined); + expect(cache.getCode(path)).toBe(undefined); + expect(cache.getSourceMap(path)).toBe(undefined); + + await cache.update(path, { + mtime, + code: 'var x = 1', + map: { foo: 'bar' }, + }); + + expect(cache.getMtime(path)).toBe(mtime); + expect(cache.getCode(path)).toBe('var x = 1'); + expect(cache.getSourceMap(path)).toEqual({ foo: 'bar' }); + expect(log.output).toMatchInlineSnapshot(` + "MISS [mtimes] foo/foo/bar.js + MISS [codes] foo/foo/bar.js + MISS [sourceMaps] foo/foo/bar.js + PUT [atimes] foo/foo/bar.js + PUT [mtimes] foo/foo/bar.js + PUT [codes] foo/foo/bar.js + PUT [sourceMaps] foo/foo/bar.js + HIT [mtimes] foo/foo/bar.js + HIT [codes] foo/foo/bar.js + HIT [sourceMaps] foo/foo/bar.js + " + `); +}); diff --git a/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts b/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts index ff6ab1c68da53..cc53294109412 100644 --- a/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts +++ b/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts @@ -39,7 +39,7 @@ import Crypto from 'crypto'; import * as babel from '@babel/core'; import { addHook } from 'pirates'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/dev-utils'; import sourceMapSupport from 'source-map-support'; import { Cache } from './cache'; @@ -134,7 +134,13 @@ export function registerNodeAutoTranspilation() { installed = true; const cache = new Cache({ + dir: Path.resolve(REPO_ROOT, 'data/node_auto_transpilation_cache_v2', UPSTREAM_BRANCH), prefix: determineCachePrefix(), + log: process.env.DEBUG_NODE_TRANSPILER_CACHE + ? Fs.createWriteStream(Path.resolve(REPO_ROOT, 'node_auto_transpilation_cache.log'), { + flags: 'a', + }) + : undefined, }); sourceMapSupport.install({ diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index cd8b1f674fa40..c62b3f2afc14d 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__(506); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(511); /* 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__(505); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(510); /* 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__(499); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(504); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -8897,9 +8897,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__(367); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(398); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(399); +/* 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); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8942,10 +8942,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__(359); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(364); -/* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(361); -/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(365); +/* 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); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -22997,7 +22997,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(249); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(246); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(313); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(318); 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; } @@ -23205,7 +23205,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return transformDependencies; }); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(252); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(read_pkg__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(300); +/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(305); /* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(write_pkg__WEBPACK_IMPORTED_MODULE_1__); 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; } @@ -26091,7 +26091,7 @@ module.exports = normalize var fixer = __webpack_require__(275) normalize.fixer = fixer -var makeWarning = __webpack_require__(298) +var makeWarning = __webpack_require__(303) var fieldsToFix = ['name','version','description','repository','modules','scripts' ,'files','bin','man','bugs','keywords','readme','homepage','license'] @@ -26136,9 +26136,9 @@ var validateLicense = __webpack_require__(277); var hostedGitInfo = __webpack_require__(282) var isBuiltinModule = __webpack_require__(286).isCore var depTypes = ["dependencies","devDependencies","optionalDependencies"] -var extractDescription = __webpack_require__(296) +var extractDescription = __webpack_require__(301) var url = __webpack_require__(283) -var typos = __webpack_require__(297) +var typos = __webpack_require__(302) var fixer = module.exports = { // default warning function @@ -30089,9 +30089,9 @@ GitHost.prototype.toString = function (opts) { /***/ (function(module, exports, __webpack_require__) { var async = __webpack_require__(287); -async.core = __webpack_require__(293); -async.isCore = __webpack_require__(292); -async.sync = __webpack_require__(295); +async.core = __webpack_require__(297); +async.isCore = __webpack_require__(299); +async.sync = __webpack_require__(300); module.exports = async; @@ -30175,6 +30175,7 @@ module.exports = function resolve(x, options, callback) { var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; + var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; @@ -30201,7 +30202,7 @@ module.exports = function resolve(x, options, callback) { if ((/\/$/).test(x) && res === basedir) { loadAsDirectory(res, opts.package, onfile); } else loadAsFile(res, opts.package, onfile); - } else if (isCore(x)) { + } else if (includeCoreModules && isCore(x)) { return cb(null, x); } else loadNodeModules(x, basedir, function (err, n, pkg) { if (err) cb(err); @@ -30582,10 +30583,75 @@ module.exports = function (x, opts) { /* 292 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(293); +"use strict"; -module.exports = function isCore(x) { - return Object.prototype.hasOwnProperty.call(core, x); + +var has = __webpack_require__(293); + +function specifierIncluded(current, specifier) { + var nodeParts = current.split('.'); + var parts = specifier.split(' '); + var op = parts.length > 1 ? parts[0] : '='; + var versionParts = (parts.length > 1 ? parts[1] : parts[0]).split('.'); + + for (var i = 0; i < 3; ++i) { + var cur = parseInt(nodeParts[i] || 0, 10); + var ver = parseInt(versionParts[i] || 0, 10); + if (cur === ver) { + continue; // eslint-disable-line no-restricted-syntax, no-continue + } + if (op === '<') { + return cur < ver; + } + if (op === '>=') { + return cur >= ver; + } + return false; + } + return op === '>='; +} + +function matchesRange(current, range) { + var specifiers = range.split(/ ?&& ?/); + if (specifiers.length === 0) { + return false; + } + for (var i = 0; i < specifiers.length; ++i) { + if (!specifierIncluded(current, specifiers[i])) { + return false; + } + } + return true; +} + +function versionIncluded(nodeVersion, specifierValue) { + if (typeof specifierValue === 'boolean') { + return specifierValue; + } + + var current = typeof nodeVersion === 'undefined' + ? process.versions && process.versions.node && process.versions.node + : nodeVersion; + + if (typeof current !== 'string') { + throw new TypeError(typeof nodeVersion === 'undefined' ? 'Unable to determine current node version' : 'If provided, a valid node version is required'); + } + + if (specifierValue && typeof specifierValue === 'object') { + for (var i = 0; i < specifierValue.length; ++i) { + if (matchesRange(current, specifierValue[i])) { + return true; + } + } + return false; + } + return matchesRange(current, specifierValue); +} + +var data = __webpack_require__(296); + +module.exports = function isCore(x, nodeVersion) { + return has(data, x) && versionIncluded(nodeVersion, data[x]); }; @@ -30593,6 +30659,95 @@ module.exports = function isCore(x) { /* 293 */ /***/ (function(module, exports, __webpack_require__) { +"use strict"; + + +var bind = __webpack_require__(294); + +module.exports = bind.call(Function.call, Object.prototype.hasOwnProperty); + + +/***/ }), +/* 294 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var implementation = __webpack_require__(295); + +module.exports = Function.prototype.bind || implementation; + + +/***/ }), +/* 295 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/* eslint no-invalid-this: 1 */ + +var ERROR_MESSAGE = 'Function.prototype.bind called on incompatible '; +var slice = Array.prototype.slice; +var toStr = Object.prototype.toString; +var funcType = '[object Function]'; + +module.exports = function bind(that) { + var target = this; + if (typeof target !== 'function' || toStr.call(target) !== funcType) { + throw new TypeError(ERROR_MESSAGE + target); + } + var args = slice.call(arguments, 1); + + var bound; + var binder = function () { + if (this instanceof bound) { + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + } + }; + + var boundLength = Math.max(0, target.length - args.length); + var boundArgs = []; + for (var i = 0; i < boundLength; i++) { + boundArgs.push('$' + i); + } + + bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder); + + if (target.prototype) { + var Empty = function Empty() {}; + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + + return bound; +}; + + +/***/ }), +/* 296 */ +/***/ (function(module) { + +module.exports = JSON.parse("{\"assert\":true,\"assert/strict\":\">= 15\",\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"diagnostics_channel\":\">= 15.1\",\"dns\":true,\"dns/promises\":\">= 15\",\"domain\":\">= 0.7.12\",\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"stream/promises\":\">= 15\",\"string_decoder\":true,\"sys\":[\">= 0.6 && < 0.7\",\">= 0.8\"],\"timers\":true,\"timers/promises\":\">= 15\",\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); + +/***/ }), +/* 297 */ +/***/ (function(module, exports, __webpack_require__) { + var current = (process.versions && process.versions.node && process.versions.node.split('.')) || []; function specifierIncluded(specifier) { @@ -30601,8 +30756,8 @@ function specifierIncluded(specifier) { var versionParts = (parts.length > 1 ? parts[1] : parts[0]).split('.'); for (var i = 0; i < 3; ++i) { - var cur = Number(current[i] || 0); - var ver = Number(versionParts[i] || 0); + var cur = parseInt(current[i] || 0, 10); + var ver = parseInt(versionParts[i] || 0, 10); if (cur === ver) { continue; // eslint-disable-line no-restricted-syntax, no-continue } @@ -30637,7 +30792,7 @@ function versionIncluded(specifierValue) { return matchesRange(specifierValue); } -var data = __webpack_require__(294); +var data = __webpack_require__(298); var core = {}; for (var mod in data) { // eslint-disable-line no-restricted-syntax @@ -30649,13 +30804,24 @@ module.exports = core; /***/ }), -/* 294 */ +/* 298 */ /***/ (function(module) { -module.exports = JSON.parse("{\"assert\":true,\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"dns\":true,\"domain\":true,\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"string_decoder\":true,\"sys\":true,\"timers\":true,\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); +module.exports = JSON.parse("{\"assert\":true,\"assert/strict\":\">= 15\",\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"diagnostics_channel\":\">= 15.1\",\"dns\":true,\"dns/promises\":\">= 15\",\"domain\":\">= 0.7.12\",\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"stream/promises\":\">= 15\",\"string_decoder\":true,\"sys\":[\">= 0.6 && < 0.7\",\">= 0.8\"],\"timers\":true,\"timers/promises\":\">= 15\",\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); /***/ }), -/* 295 */ +/* 299 */ +/***/ (function(module, exports, __webpack_require__) { + +var isCoreModule = __webpack_require__(292); + +module.exports = function isCore(x) { + return isCoreModule(x); +}; + + +/***/ }), +/* 300 */ /***/ (function(module, exports, __webpack_require__) { var isCore = __webpack_require__(292); @@ -30726,6 +30892,7 @@ module.exports = function resolveSync(x, options) { var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; + var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; @@ -30739,7 +30906,7 @@ module.exports = function resolveSync(x, options) { if (x === '.' || x === '..' || x.slice(-1) === '/') res += '/'; var m = loadAsFileSync(res) || loadAsDirectorySync(res); if (m) return maybeRealpathSync(realpathSync, m, opts); - } else if (isCore(x)) { + } else if (includeCoreModules && isCore(x)) { return x; } else { var n = loadNodeModulesSync(x, absoluteStart); @@ -30852,7 +31019,7 @@ module.exports = function resolveSync(x, options) { /***/ }), -/* 296 */ +/* 301 */ /***/ (function(module, exports) { module.exports = extractDescription @@ -30872,17 +31039,17 @@ function extractDescription (d) { /***/ }), -/* 297 */ +/* 302 */ /***/ (function(module) { module.exports = JSON.parse("{\"topLevel\":{\"dependancies\":\"dependencies\",\"dependecies\":\"dependencies\",\"depdenencies\":\"dependencies\",\"devEependencies\":\"devDependencies\",\"depends\":\"dependencies\",\"dev-dependencies\":\"devDependencies\",\"devDependences\":\"devDependencies\",\"devDepenencies\":\"devDependencies\",\"devdependencies\":\"devDependencies\",\"repostitory\":\"repository\",\"repo\":\"repository\",\"prefereGlobal\":\"preferGlobal\",\"hompage\":\"homepage\",\"hampage\":\"homepage\",\"autohr\":\"author\",\"autor\":\"author\",\"contributers\":\"contributors\",\"publicationConfig\":\"publishConfig\",\"script\":\"scripts\"},\"bugs\":{\"web\":\"url\",\"name\":\"url\"},\"script\":{\"server\":\"start\",\"tests\":\"test\"}}"); /***/ }), -/* 298 */ +/* 303 */ /***/ (function(module, exports, __webpack_require__) { var util = __webpack_require__(112) -var messages = __webpack_require__(299) +var messages = __webpack_require__(304) module.exports = function() { var args = Array.prototype.slice.call(arguments, 0) @@ -30907,20 +31074,20 @@ function makeTypoWarning (providedName, probableName, field) { /***/ }), -/* 299 */ +/* 304 */ /***/ (function(module) { module.exports = JSON.parse("{\"repositories\":\"'repositories' (plural) Not supported. Please pick one as the 'repository' field\",\"missingRepository\":\"No repository field.\",\"brokenGitUrl\":\"Probably broken git url: %s\",\"nonObjectScripts\":\"scripts must be an object\",\"nonStringScript\":\"script values must be string commands\",\"nonArrayFiles\":\"Invalid 'files' member\",\"invalidFilename\":\"Invalid filename in 'files' list: %s\",\"nonArrayBundleDependencies\":\"Invalid 'bundleDependencies' list. Must be array of package names\",\"nonStringBundleDependency\":\"Invalid bundleDependencies member: %s\",\"nonDependencyBundleDependency\":\"Non-dependency in bundleDependencies: %s\",\"nonObjectDependencies\":\"%s field must be an object\",\"nonStringDependency\":\"Invalid dependency: %s %s\",\"deprecatedArrayDependencies\":\"specifying %s as array is deprecated\",\"deprecatedModules\":\"modules field is deprecated\",\"nonArrayKeywords\":\"keywords should be an array of strings\",\"nonStringKeyword\":\"keywords should be an array of strings\",\"conflictingName\":\"%s is also the name of a node core module.\",\"nonStringDescription\":\"'description' field should be a string\",\"missingDescription\":\"No description\",\"missingReadme\":\"No README data\",\"missingLicense\":\"No license field.\",\"nonEmailUrlBugsString\":\"Bug string field must be url, email, or {email,url}\",\"nonUrlBugsUrlField\":\"bugs.url field must be a string url. Deleted.\",\"nonEmailBugsEmailField\":\"bugs.email field must be a string email. Deleted.\",\"emptyNormalizedBugs\":\"Normalized value of bugs field is an empty object. Deleted.\",\"nonUrlHomepage\":\"homepage field must be a string url. Deleted.\",\"invalidLicense\":\"license should be a valid SPDX license expression\",\"typo\":\"%s should probably be %s.\"}"); /***/ }), -/* 300 */ +/* 305 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const writeJsonFile = __webpack_require__(301); -const sortKeys = __webpack_require__(307); +const writeJsonFile = __webpack_require__(306); +const sortKeys = __webpack_require__(312); const dependencyKeys = new Set([ 'dependencies', @@ -30985,18 +31152,18 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 301 */ +/* 306 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const fs = __webpack_require__(133); -const writeFileAtomic = __webpack_require__(302); -const sortKeys = __webpack_require__(307); -const makeDir = __webpack_require__(309); -const pify = __webpack_require__(310); -const detectIndent = __webpack_require__(312); +const writeFileAtomic = __webpack_require__(307); +const sortKeys = __webpack_require__(312); +const makeDir = __webpack_require__(314); +const pify = __webpack_require__(315); +const detectIndent = __webpack_require__(317); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -31068,7 +31235,7 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 302 */ +/* 307 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31079,8 +31246,8 @@ module.exports._getTmpname = getTmpname // for testing module.exports._cleanupOnExit = cleanupOnExit var fs = __webpack_require__(133) -var MurmurHash3 = __webpack_require__(303) -var onExit = __webpack_require__(304) +var MurmurHash3 = __webpack_require__(308) +var onExit = __webpack_require__(309) var path = __webpack_require__(4) var activeFiles = {} @@ -31088,7 +31255,7 @@ var activeFiles = {} /* istanbul ignore next */ var threadId = (function getId () { try { - var workerThreads = __webpack_require__(306) + var workerThreads = __webpack_require__(311) /// if we are in main thread, this is set to `0` return workerThreads.threadId @@ -31313,7 +31480,7 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 303 */ +/* 308 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -31455,14 +31622,14 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 304 */ +/* 309 */ /***/ (function(module, exports, __webpack_require__) { // Note: since nyc uses this module to output coverage, any lines // that are in the direct sync flow of nyc's outputCoverage are // ignored, since we can never get coverage for them. var assert = __webpack_require__(140) -var signals = __webpack_require__(305) +var signals = __webpack_require__(310) var EE = __webpack_require__(156) /* istanbul ignore if */ @@ -31618,7 +31785,7 @@ function processEmit (ev, arg) { /***/ }), -/* 305 */ +/* 310 */ /***/ (function(module, exports) { // This is not the set of all possible signals. @@ -31677,18 +31844,18 @@ if (process.platform === 'linux') { /***/ }), -/* 306 */ +/* 311 */ /***/ (function(module, exports) { module.exports = require(undefined); /***/ }), -/* 307 */ +/* 312 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isPlainObj = __webpack_require__(308); +const isPlainObj = __webpack_require__(313); module.exports = (obj, opts) => { if (!isPlainObj(obj)) { @@ -31745,7 +31912,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 308 */ +/* 313 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31759,15 +31926,15 @@ module.exports = function (x) { /***/ }), -/* 309 */ +/* 314 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const pify = __webpack_require__(310); -const semver = __webpack_require__(311); +const pify = __webpack_require__(315); +const semver = __webpack_require__(316); const defaults = { mode: 0o777 & (~process.umask()), @@ -31905,7 +32072,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 310 */ +/* 315 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31980,7 +32147,7 @@ module.exports = (input, options) => { /***/ }), -/* 311 */ +/* 316 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -33469,7 +33636,7 @@ function coerce (version) { /***/ }), -/* 312 */ +/* 317 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -33598,7 +33765,7 @@ module.exports = str => { /***/ }), -/* 313 */ +/* 318 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -33606,7 +33773,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "installInDir", function() { return installInDir; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(314); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(319); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -33669,7 +33836,7 @@ function runScriptInPackageStreaming({ } /***/ }), -/* 314 */ +/* 319 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -33680,9 +33847,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var stream__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(stream__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(113); /* 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__(315); +/* 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__(351); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(356); /* 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; } @@ -33770,23 +33937,23 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 315 */ +/* 320 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const childProcess = __webpack_require__(316); -const crossSpawn = __webpack_require__(317); -const stripFinalNewline = __webpack_require__(330); -const npmRunPath = __webpack_require__(331); -const onetime = __webpack_require__(333); -const makeError = __webpack_require__(335); -const normalizeStdio = __webpack_require__(340); -const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(341); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(342); -const {mergePromise, getSpawnedPromise} = __webpack_require__(349); -const {joinCommand, parseCommand} = __webpack_require__(350); +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 DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; @@ -34033,21 +34200,21 @@ module.exports.node = (scriptPath, args, options = {}) => { /***/ }), -/* 316 */ +/* 321 */ /***/ (function(module, exports) { module.exports = require("child_process"); /***/ }), -/* 317 */ +/* 322 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const cp = __webpack_require__(316); -const parse = __webpack_require__(318); -const enoent = __webpack_require__(329); +const cp = __webpack_require__(321); +const parse = __webpack_require__(323); +const enoent = __webpack_require__(334); function spawn(command, args, options) { // Parse the arguments @@ -34085,16 +34252,16 @@ module.exports._enoent = enoent; /***/ }), -/* 318 */ +/* 323 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const resolveCommand = __webpack_require__(319); -const escape = __webpack_require__(325); -const readShebang = __webpack_require__(326); +const resolveCommand = __webpack_require__(324); +const escape = __webpack_require__(330); +const readShebang = __webpack_require__(331); const isWin = process.platform === 'win32'; const isExecutableRegExp = /\.(?:com|exe)$/i; @@ -34183,15 +34350,15 @@ module.exports = parse; /***/ }), -/* 319 */ +/* 324 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const which = __webpack_require__(320); -const pathKey = __webpack_require__(324)(); +const which = __webpack_require__(325); +const pathKey = __webpack_require__(329)(); function resolveCommandAttempt(parsed, withoutPathExt) { const cwd = process.cwd(); @@ -34241,7 +34408,7 @@ module.exports = resolveCommand; /***/ }), -/* 320 */ +/* 325 */ /***/ (function(module, exports, __webpack_require__) { const isWindows = process.platform === 'win32' || @@ -34250,7 +34417,7 @@ const isWindows = process.platform === 'win32' || const path = __webpack_require__(4) const COLON = isWindows ? ';' : ':' -const isexe = __webpack_require__(321) +const isexe = __webpack_require__(326) const getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) @@ -34372,15 +34539,15 @@ which.sync = whichSync /***/ }), -/* 321 */ +/* 326 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(134) var core if (process.platform === 'win32' || global.TESTING_WINDOWS) { - core = __webpack_require__(322) + core = __webpack_require__(327) } else { - core = __webpack_require__(323) + core = __webpack_require__(328) } module.exports = isexe @@ -34435,7 +34602,7 @@ function sync (path, options) { /***/ }), -/* 322 */ +/* 327 */ /***/ (function(module, exports, __webpack_require__) { module.exports = isexe @@ -34483,7 +34650,7 @@ function sync (path, options) { /***/ }), -/* 323 */ +/* 328 */ /***/ (function(module, exports, __webpack_require__) { module.exports = isexe @@ -34530,7 +34697,7 @@ function checkMode (stat, options) { /***/ }), -/* 324 */ +/* 329 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34553,7 +34720,7 @@ module.exports.default = pathKey; /***/ }), -/* 325 */ +/* 330 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34605,14 +34772,14 @@ module.exports.argument = escapeArgument; /***/ }), -/* 326 */ +/* 331 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const shebangCommand = __webpack_require__(327); +const shebangCommand = __webpack_require__(332); function readShebang(command) { // Read the first 150 bytes from the file @@ -34635,12 +34802,12 @@ module.exports = readShebang; /***/ }), -/* 327 */ +/* 332 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const shebangRegex = __webpack_require__(328); +const shebangRegex = __webpack_require__(333); module.exports = (string = '') => { const match = string.match(shebangRegex); @@ -34661,7 +34828,7 @@ module.exports = (string = '') => { /***/ }), -/* 328 */ +/* 333 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34670,7 +34837,7 @@ module.exports = /^#!(.*)/; /***/ }), -/* 329 */ +/* 334 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34736,7 +34903,7 @@ module.exports = { /***/ }), -/* 330 */ +/* 335 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34759,13 +34926,13 @@ module.exports = input => { /***/ }), -/* 331 */ +/* 336 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathKey = __webpack_require__(332); +const pathKey = __webpack_require__(337); const npmRunPath = options => { options = { @@ -34813,7 +34980,7 @@ module.exports.env = options => { /***/ }), -/* 332 */ +/* 337 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34836,12 +35003,12 @@ module.exports.default = pathKey; /***/ }), -/* 333 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(334); +const mimicFn = __webpack_require__(339); const calledFunctions = new WeakMap(); @@ -34893,7 +35060,7 @@ module.exports.callCount = fn => { /***/ }), -/* 334 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34913,12 +35080,12 @@ module.exports.default = mimicFn; /***/ }), -/* 335 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const {signalsByName} = __webpack_require__(336); +const {signalsByName} = __webpack_require__(341); const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { if (timedOut) { @@ -35006,14 +35173,14 @@ module.exports = makeError; /***/ }), -/* 336 */ +/* 341 */ /***/ (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__(337); -var _realtime=__webpack_require__(339); +var _signals=__webpack_require__(342); +var _realtime=__webpack_require__(344); @@ -35083,14 +35250,14 @@ const signalsByNumber=getSignalsByNumber();exports.signalsByNumber=signalsByNumb //# sourceMappingURL=main.js.map /***/ }), -/* 337 */ +/* 342 */ /***/ (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__(338); -var _realtime=__webpack_require__(339); +var _core=__webpack_require__(343); +var _realtime=__webpack_require__(344); @@ -35124,7 +35291,7 @@ return{name,number,description,supported,action,forced,standard}; //# sourceMappingURL=signals.js.map /***/ }), -/* 338 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35403,7 +35570,7 @@ standard:"other"}];exports.SIGNALS=SIGNALS; //# sourceMappingURL=core.js.map /***/ }), -/* 339 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35428,7 +35595,7 @@ const SIGRTMAX=64;exports.SIGRTMAX=SIGRTMAX; //# sourceMappingURL=realtime.js.map /***/ }), -/* 340 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35487,13 +35654,13 @@ module.exports.node = opts => { /***/ }), -/* 341 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const os = __webpack_require__(121); -const onExit = __webpack_require__(304); +const onExit = __webpack_require__(309); const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; @@ -35606,14 +35773,14 @@ module.exports = { /***/ }), -/* 342 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isStream = __webpack_require__(343); -const getStream = __webpack_require__(344); -const mergeStream = __webpack_require__(348); +const isStream = __webpack_require__(348); +const getStream = __webpack_require__(349); +const mergeStream = __webpack_require__(353); // `input` option const handleInput = (spawned, input) => { @@ -35710,7 +35877,7 @@ module.exports = { /***/ }), -/* 343 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35746,13 +35913,13 @@ module.exports = isStream; /***/ }), -/* 344 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pump = __webpack_require__(345); -const bufferStream = __webpack_require__(347); +const pump = __webpack_require__(350); +const bufferStream = __webpack_require__(352); class MaxBufferError extends Error { constructor() { @@ -35811,11 +35978,11 @@ module.exports.MaxBufferError = MaxBufferError; /***/ }), -/* 345 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(162) -var eos = __webpack_require__(346) +var eos = __webpack_require__(351) var fs = __webpack_require__(134) // we only need fs to get the ReadStream and WriteStream prototypes var noop = function () {} @@ -35899,7 +36066,7 @@ module.exports = pump /***/ }), -/* 346 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(162); @@ -35999,7 +36166,7 @@ module.exports = eos; /***/ }), -/* 347 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36058,7 +36225,7 @@ module.exports = options => { /***/ }), -/* 348 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36106,7 +36273,7 @@ module.exports = function (/*streams...*/) { /***/ }), -/* 349 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36159,7 +36326,7 @@ module.exports = { /***/ }), -/* 350 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36204,7 +36371,7 @@ module.exports = { /***/ }), -/* 351 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -36212,12 +36379,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__(352); -module.exports.cli = __webpack_require__(356); +module.exports = __webpack_require__(357); +module.exports.cli = __webpack_require__(361); /***/ }), -/* 352 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36232,9 +36399,9 @@ var stream = __webpack_require__(138); var util = __webpack_require__(112); var fs = __webpack_require__(134); -var through = __webpack_require__(353); -var duplexer = __webpack_require__(354); -var StringDecoder = __webpack_require__(355).StringDecoder; +var through = __webpack_require__(358); +var duplexer = __webpack_require__(359); +var StringDecoder = __webpack_require__(360).StringDecoder; module.exports = Logger; @@ -36423,7 +36590,7 @@ function lineMerger(host) { /***/ }), -/* 353 */ +/* 358 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -36537,7 +36704,7 @@ function through (write, end, opts) { /***/ }), -/* 354 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -36630,13 +36797,13 @@ function duplex(writer, reader) { /***/ }), -/* 355 */ +/* 360 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 356 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36647,11 +36814,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(357); +var minimist = __webpack_require__(362); var path = __webpack_require__(4); -var Logger = __webpack_require__(352); -var pkg = __webpack_require__(358); +var Logger = __webpack_require__(357); +var pkg = __webpack_require__(363); module.exports = cli; @@ -36705,7 +36872,7 @@ function usage($0, p) { /***/ }), -/* 357 */ +/* 362 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -36956,13 +37123,13 @@ function isNumber (x) { /***/ }), -/* 358 */ +/* 363 */ /***/ (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\"}}"); /***/ }), -/* 359 */ +/* 364 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36970,13 +37137,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__(360); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(365); /* 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__(315); +/* 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__(361); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(366); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37175,20 +37342,20 @@ async function getAllChecksums(kbn, log, yarnLock) { } /***/ }), -/* 360 */ +/* 365 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 361 */ +/* 366 */ /***/ (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__(362); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(367); /* 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); /* @@ -37301,7 +37468,7 @@ function resolveDepsForProject({ } /***/ }), -/* 362 */ +/* 367 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -38860,7 +39027,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(360); +module.exports = __webpack_require__(365); /***/ }), /* 10 */, @@ -41184,7 +41351,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(363); +module.exports = __webpack_require__(368); /***/ }), /* 64 */, @@ -47579,13 +47746,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 363 */ +/* 368 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 364 */ +/* 369 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47682,13 +47849,13 @@ class BootstrapCacheFile { } /***/ }), -/* 365 */ +/* 370 */ /***/ (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__(362); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(367); /* 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__); @@ -47699,7 +47866,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__(366); +/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(371); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -47891,7 +48058,7 @@ function getDevOnlyProductionDepsTree(kbn, projectName) { } /***/ }), -/* 366 */ +/* 371 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -48044,7 +48211,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 367 */ +/* 372 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -48052,7 +48219,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__(368); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(373); /* 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__); @@ -48152,20 +48319,20 @@ const CleanCommand = { }; /***/ }), -/* 368 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(369); -const chalk = __webpack_require__(370); -const cliCursor = __webpack_require__(377); -const cliSpinners = __webpack_require__(379); -const logSymbols = __webpack_require__(381); -const stripAnsi = __webpack_require__(390); -const wcwidth = __webpack_require__(392); -const isInteractive = __webpack_require__(396); -const MuteStream = __webpack_require__(397); +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 TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -48518,23 +48685,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 369 */ +/* 374 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 370 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(371); +const ansiStyles = __webpack_require__(376); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(375); +} = __webpack_require__(380); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -48735,7 +48902,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(376); + template = __webpack_require__(381); } return template(chalk, parts.join('')); @@ -48764,7 +48931,7 @@ module.exports = chalk; /***/ }), -/* 371 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48810,7 +48977,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(372); + colorConvert = __webpack_require__(377); } const offset = isBackground ? 10 : 0; @@ -48935,11 +49102,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 372 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(373); -const route = __webpack_require__(374); +const conversions = __webpack_require__(378); +const route = __webpack_require__(379); const convert = {}; @@ -49022,7 +49189,7 @@ module.exports = convert; /***/ }), -/* 373 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -49867,10 +50034,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 374 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(373); +const conversions = __webpack_require__(378); /* This function routes a model to all other models. @@ -49970,7 +50137,7 @@ module.exports = function (fromModel) { /***/ }), -/* 375 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50016,7 +50183,7 @@ module.exports = { /***/ }), -/* 376 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50157,12 +50324,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 377 */ +/* 382 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(378); +const restoreCursor = __webpack_require__(383); let isHidden = false; @@ -50199,13 +50366,13 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 378 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(333); -const signalExit = __webpack_require__(304); +const onetime = __webpack_require__(338); +const signalExit = __webpack_require__(309); module.exports = onetime(() => { signalExit(() => { @@ -50215,13 +50382,13 @@ module.exports = onetime(() => { /***/ }), -/* 379 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(380)); +const spinners = Object.assign({}, __webpack_require__(385)); const spinnersList = Object.keys(spinners); @@ -50239,18 +50406,18 @@ module.exports.default = spinners; /***/ }), -/* 380 */ +/* 385 */ /***/ (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\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]}}"); /***/ }), -/* 381 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(382); +const chalk = __webpack_require__(387); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -50272,16 +50439,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 382 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(265); -const ansiStyles = __webpack_require__(383); -const stdoutColor = __webpack_require__(388).stdout; +const ansiStyles = __webpack_require__(388); +const stdoutColor = __webpack_require__(393).stdout; -const template = __webpack_require__(389); +const template = __webpack_require__(394); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -50507,12 +50674,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 383 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(384); +const colorConvert = __webpack_require__(389); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -50680,11 +50847,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 384 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(385); -var route = __webpack_require__(387); +var conversions = __webpack_require__(390); +var route = __webpack_require__(392); var convert = {}; @@ -50764,11 +50931,11 @@ module.exports = convert; /***/ }), -/* 385 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(386); +var cssKeywords = __webpack_require__(391); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -51638,7 +51805,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 386 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51797,10 +51964,10 @@ module.exports = { /***/ }), -/* 387 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(385); +var conversions = __webpack_require__(390); /* this function routes a model to all other models. @@ -51900,7 +52067,7 @@ module.exports = function (fromModel) { /***/ }), -/* 388 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52038,7 +52205,7 @@ module.exports = { /***/ }), -/* 389 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52173,18 +52340,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 390 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(391); +const ansiRegex = __webpack_require__(396); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 391 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52201,14 +52368,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 392 */ +/* 397 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(393) -var combining = __webpack_require__(395) +var defaults = __webpack_require__(398) +var combining = __webpack_require__(400) var DEFAULTS = { nul: 0, @@ -52307,10 +52474,10 @@ function bisearch(ucs) { /***/ }), -/* 393 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(394); +var clone = __webpack_require__(399); module.exports = function(options, defaults) { options = options || {}; @@ -52325,7 +52492,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 394 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -52497,7 +52664,7 @@ if ( true && module.exports) { /***/ }), -/* 395 */ +/* 400 */ /***/ (function(module, exports) { module.exports = [ @@ -52553,7 +52720,7 @@ module.exports = [ /***/ }), -/* 396 */ +/* 401 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52569,7 +52736,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 397 */ +/* 402 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -52720,7 +52887,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 398 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52781,7 +52948,7 @@ const RunCommand = { }; /***/ }), -/* 399 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52791,7 +52958,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__(400); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(405); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -52877,14 +53044,14 @@ const WatchCommand = { }; /***/ }), -/* 400 */ +/* 405 */ /***/ (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__(401); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(406); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -52951,141 +53118,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 401 */ +/* 406 */ /***/ (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__(402); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(407); /* 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__(403); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(408); /* 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__(404); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(409); /* 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__(405); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(410); /* 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__(406); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(411); /* 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__(407); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(412); /* 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__(408); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(413); /* 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__(409); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(414); /* 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__(410); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(415); /* 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__(411); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(416); /* 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__(412); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(417); /* 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__(413); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(418); /* 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__(414); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(419); /* 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__(415); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(420); /* 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__(416); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(421); /* 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__(417); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(422); /* 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__(418); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(423); /* 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__(419); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(424); /* 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__(421); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(426); /* 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__(422); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(427); /* 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__(423); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(428); /* 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__(424); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(429); /* 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__(425); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(430); /* 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__(426); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(431); /* 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__(429); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(434); /* 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__(430); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(435); /* 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__(431); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(436); /* 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__(432); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(437); /* 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__(433); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(438); /* 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__(434); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(439); /* 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__(435); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(440); /* 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__(436); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(441); /* 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__(437); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(442); /* 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__(438); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(443); /* 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__(439); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(444); /* 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__(440); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(445); /* 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__(442); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(447); /* 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__(443); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(448); /* 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__(444); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(449); /* 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__(447); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(452); /* 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); @@ -53096,175 +53263,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__(448); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(453); /* 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__(449); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(454); /* 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__(450); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(455); /* 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__(451); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(456); /* 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__(452); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(457); /* 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__(453); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(458); /* 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__(454); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(459); /* 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__(455); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(460); /* 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__(456); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(461); /* 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__(457); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(462); /* 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__(458); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(463); /* 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__(459); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(464); /* 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__(460); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(465); /* 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__(445); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(450); /* 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__(461); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(466); /* 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__(462); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(467); /* 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__(463); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(468); /* 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__(464); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(469); /* 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__(465); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(470); /* 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__(466); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(471); /* 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__(446); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(451); /* 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__(467); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(472); /* 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__(468); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(473); /* 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__(469); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(474); /* 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__(470); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(475); /* 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__(471); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(476); /* 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__(472); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(477); /* 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__(473); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(478); /* 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__(474); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(479); /* 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__(475); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(480); /* 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__(476); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(481); /* 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__(478); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(483); /* 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__(479); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(484); /* 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__(480); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(485); /* 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__(428); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(433); /* 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__(441); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(446); /* 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__(481); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(486); /* 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__(482); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(487); /* 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__(483); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(488); /* 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__(484); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(489); /* 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__(485); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(490); /* 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__(427); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(432); /* 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__(486); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(491); /* 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__(487); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(492); /* 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__(488); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(493); /* 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__(489); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(494); /* 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__(490); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(495); /* 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__(491); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(496); /* 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__(492); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(497); /* 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__(493); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(498); /* 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__(494); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(499); /* 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__(495); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(500); /* 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__(496); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(501); /* 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__(497); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(502); /* 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__(498); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(503); /* 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 */ @@ -53375,7 +53542,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 402 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53454,14 +53621,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 403 */ +/* 408 */ /***/ (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__(402); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(407); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -53477,7 +53644,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 404 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53524,7 +53691,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 405 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53625,7 +53792,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 406 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53786,7 +53953,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 407 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53905,7 +54072,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 408 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53998,7 +54165,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54058,7 +54225,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54074,7 +54241,7 @@ function combineAll(project) { /***/ }), -/* 411 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54106,7 +54273,7 @@ function combineLatest() { /***/ }), -/* 412 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54126,7 +54293,7 @@ function concat() { /***/ }), -/* 413 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54142,13 +54309,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 414 */ +/* 419 */ /***/ (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__(413); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(418); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -54158,7 +54325,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 415 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54223,7 +54390,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 416 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54308,7 +54475,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 417 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54384,7 +54551,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 418 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54434,7 +54601,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 419 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54442,7 +54609,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__(420); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* 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 */ @@ -54541,7 +54708,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 420 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54555,7 +54722,7 @@ function isDate(value) { /***/ }), -/* 421 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54701,7 +54868,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 422 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54739,7 +54906,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54815,7 +54982,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54886,13 +55053,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 425 */ +/* 430 */ /***/ (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__(424); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(429); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -54902,7 +55069,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 426 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54910,9 +55077,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__(427); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(418); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(428); +/* 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); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -54934,7 +55101,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 427 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55000,7 +55167,7 @@ function defaultErrorFactory() { /***/ }), -/* 428 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55062,7 +55229,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 429 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55084,7 +55251,7 @@ function endWith() { /***/ }), -/* 430 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55146,7 +55313,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 431 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55200,7 +55367,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 432 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55294,7 +55461,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55406,7 +55573,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55444,7 +55611,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 435 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55516,13 +55683,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 441 */ /***/ (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__(435); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(440); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -55532,7 +55699,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 437 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55540,9 +55707,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__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(418); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(427); +/* 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 _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55559,7 +55726,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 438 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55596,7 +55763,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 439 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55640,7 +55807,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55648,9 +55815,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__(441); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(427); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(418); +/* 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 _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55667,7 +55834,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 441 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55744,7 +55911,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 442 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55783,7 +55950,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55833,13 +56000,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 449 */ /***/ (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__(445); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -55852,15 +56019,15 @@ function max(comparer) { /***/ }), -/* 445 */ +/* 450 */ /***/ (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__(446); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(441); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(418); +/* 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 _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -55881,7 +56048,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 446 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55963,7 +56130,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 447 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55983,7 +56150,7 @@ function merge() { /***/ }), -/* 448 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56008,7 +56175,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 449 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56117,13 +56284,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 450 */ +/* 455 */ /***/ (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__(445); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -56136,7 +56303,7 @@ function min(comparer) { /***/ }), -/* 451 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56185,7 +56352,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 452 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56275,7 +56442,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 453 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56323,7 +56490,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56346,7 +56513,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 455 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56386,14 +56553,14 @@ function plucker(props, length) { /***/ }), -/* 456 */ +/* 461 */ /***/ (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__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -56406,14 +56573,14 @@ function publish(selector) { /***/ }), -/* 457 */ +/* 462 */ /***/ (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__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -56424,14 +56591,14 @@ function publishBehavior(value) { /***/ }), -/* 458 */ +/* 463 */ /***/ (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__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -56442,14 +56609,14 @@ function publishLast() { /***/ }), -/* 459 */ +/* 464 */ /***/ (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__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -56465,7 +56632,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 460 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56492,7 +56659,7 @@ function race() { /***/ }), -/* 461 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56557,7 +56724,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 462 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56651,7 +56818,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 463 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56704,7 +56871,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 464 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56790,7 +56957,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 465 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56845,7 +57012,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 466 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56905,7 +57072,7 @@ function dispatchNotification(state) { /***/ }), -/* 467 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57028,13 +57195,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 468 */ +/* 473 */ /***/ (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__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(456); /* 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 */ @@ -57051,7 +57218,7 @@ function share() { /***/ }), -/* 469 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57120,7 +57287,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 470 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57200,7 +57367,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 471 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57242,7 +57409,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57304,7 +57471,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57361,7 +57528,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 474 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57417,7 +57584,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57446,13 +57613,13 @@ function startWith() { /***/ }), -/* 476 */ +/* 481 */ /***/ (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__(477); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(482); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -57477,7 +57644,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 477 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57541,13 +57708,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 478 */ +/* 483 */ /***/ (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__(479); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(484); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -57559,7 +57726,7 @@ function switchAll() { /***/ }), -/* 479 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57647,13 +57814,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 480 */ +/* 485 */ /***/ (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__(479); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(484); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -57663,7 +57830,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 481 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57711,7 +57878,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 482 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57779,7 +57946,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57867,7 +58034,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 484 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57969,7 +58136,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57978,7 +58145,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__(484); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(489); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -58067,7 +58234,7 @@ function dispatchNext(arg) { /***/ }), -/* 486 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58075,7 +58242,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__(446); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); /* 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 */ @@ -58111,7 +58278,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 487 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58119,7 +58286,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__(488); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(493); /* 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 */ @@ -58136,7 +58303,7 @@ function timeout(due, scheduler) { /***/ }), -/* 488 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58144,7 +58311,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__(420); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -58215,7 +58382,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 489 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58245,13 +58412,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 490 */ +/* 495 */ /***/ (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__(445); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -58268,7 +58435,7 @@ function toArray() { /***/ }), -/* 491 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58346,7 +58513,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 492 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58436,7 +58603,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 493 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58606,7 +58773,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 494 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58749,7 +58916,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 495 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58846,7 +59013,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 496 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58941,7 +59108,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58963,7 +59130,7 @@ function zip() { /***/ }), -/* 498 */ +/* 503 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58979,7 +59146,7 @@ function zipAll(project) { /***/ }), -/* 499 */ +/* 504 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58988,8 +59155,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__(366); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); +/* 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); 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; } @@ -59071,7 +59238,7 @@ function toArray(value) { } /***/ }), -/* 500 */ +/* 505 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59079,13 +59246,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__(501); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(506); /* 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__(361); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(366); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(505); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(510); 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; } @@ -59247,15 +59414,15 @@ class Kibana { } /***/ }), -/* 501 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(150); -const arrayUnion = __webpack_require__(502); -const arrayDiffer = __webpack_require__(503); -const arrify = __webpack_require__(504); +const arrayUnion = __webpack_require__(507); +const arrayDiffer = __webpack_require__(508); +const arrify = __webpack_require__(509); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -59279,7 +59446,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 502 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59291,7 +59458,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 503 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59306,7 +59473,7 @@ module.exports = arrayDiffer; /***/ }), -/* 504 */ +/* 509 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59336,7 +59503,7 @@ module.exports = arrify; /***/ }), -/* 505 */ +/* 510 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59406,12 +59573,12 @@ function getProjectPaths({ } /***/ }), -/* 506 */ +/* 511 */ /***/ (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__(507); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); /* @@ -59435,19 +59602,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 507 */ +/* 512 */ /***/ (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__(508); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(513); /* 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__(505); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(510); /* 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); @@ -59584,7 +59751,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 508 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59592,14 +59759,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__(509); -const arrify = __webpack_require__(504); -const globby = __webpack_require__(510); -const hasGlob = __webpack_require__(706); -const cpFile = __webpack_require__(708); -const junk = __webpack_require__(718); -const pFilter = __webpack_require__(719); -const CpyError = __webpack_require__(721); +const pMap = __webpack_require__(514); +const arrify = __webpack_require__(509); +const globby = __webpack_require__(515); +const hasGlob = __webpack_require__(711); +const cpFile = __webpack_require__(713); +const junk = __webpack_require__(723); +const pFilter = __webpack_require__(724); +const CpyError = __webpack_require__(726); const defaultOptions = { ignoreJunk: true @@ -59750,7 +59917,7 @@ module.exports = (source, destination, { /***/ }), -/* 509 */ +/* 514 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59838,17 +60005,17 @@ module.exports = async ( /***/ }), -/* 510 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(511); +const arrayUnion = __webpack_require__(516); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(513); -const dirGlob = __webpack_require__(699); -const gitignore = __webpack_require__(702); +const fastGlob = __webpack_require__(518); +const dirGlob = __webpack_require__(704); +const gitignore = __webpack_require__(707); const DEFAULT_FILTER = () => false; @@ -59993,12 +60160,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 511 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(512); +var arrayUniq = __webpack_require__(517); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -60006,7 +60173,7 @@ module.exports = function () { /***/ }), -/* 512 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60075,10 +60242,10 @@ if ('Set' in global) { /***/ }), -/* 513 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(514); +const pkg = __webpack_require__(519); module.exports = pkg.async; module.exports.default = pkg.async; @@ -60091,19 +60258,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 514 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(515); -var taskManager = __webpack_require__(516); -var reader_async_1 = __webpack_require__(670); -var reader_stream_1 = __webpack_require__(694); -var reader_sync_1 = __webpack_require__(695); -var arrayUtils = __webpack_require__(697); -var streamUtils = __webpack_require__(698); +var optionsManager = __webpack_require__(520); +var taskManager = __webpack_require__(521); +var reader_async_1 = __webpack_require__(675); +var reader_stream_1 = __webpack_require__(699); +var reader_sync_1 = __webpack_require__(700); +var arrayUtils = __webpack_require__(702); +var streamUtils = __webpack_require__(703); /** * Synchronous API. */ @@ -60169,7 +60336,7 @@ function isString(source) { /***/ }), -/* 515 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60207,13 +60374,13 @@ exports.prepare = prepare; /***/ }), -/* 516 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(517); +var patternUtils = __webpack_require__(522); /** * Generate tasks based on parent directory of each pattern. */ @@ -60304,16 +60471,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 517 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(518); +var globParent = __webpack_require__(523); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(521); +var micromatch = __webpack_require__(526); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -60459,15 +60626,15 @@ exports.matchAny = matchAny; /***/ }), -/* 518 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(519); -var pathDirname = __webpack_require__(520); +var isglob = __webpack_require__(524); +var pathDirname = __webpack_require__(525); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -60490,7 +60657,7 @@ module.exports = function globParent(str) { /***/ }), -/* 519 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -60521,7 +60688,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 520 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60671,7 +60838,7 @@ module.exports.win32 = win32; /***/ }), -/* 521 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60682,18 +60849,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(522); -var toRegex = __webpack_require__(523); -var extend = __webpack_require__(636); +var braces = __webpack_require__(527); +var toRegex = __webpack_require__(528); +var extend = __webpack_require__(641); /** * Local dependencies */ -var compilers = __webpack_require__(638); -var parsers = __webpack_require__(665); -var cache = __webpack_require__(666); -var utils = __webpack_require__(667); +var compilers = __webpack_require__(643); +var parsers = __webpack_require__(670); +var cache = __webpack_require__(671); +var utils = __webpack_require__(672); var MAX_LENGTH = 1024 * 64; /** @@ -61555,7 +61722,7 @@ module.exports = micromatch; /***/ }), -/* 522 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61565,18 +61732,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(523); -var unique = __webpack_require__(545); -var extend = __webpack_require__(546); +var toRegex = __webpack_require__(528); +var unique = __webpack_require__(550); +var extend = __webpack_require__(551); /** * Local dependencies */ -var compilers = __webpack_require__(548); -var parsers = __webpack_require__(561); -var Braces = __webpack_require__(565); -var utils = __webpack_require__(549); +var compilers = __webpack_require__(553); +var parsers = __webpack_require__(566); +var Braces = __webpack_require__(570); +var utils = __webpack_require__(554); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -61880,16 +62047,16 @@ module.exports = braces; /***/ }), -/* 523 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(524); -var define = __webpack_require__(530); -var extend = __webpack_require__(538); -var not = __webpack_require__(542); +var safe = __webpack_require__(529); +var define = __webpack_require__(535); +var extend = __webpack_require__(543); +var not = __webpack_require__(547); var MAX_LENGTH = 1024 * 64; /** @@ -62042,10 +62209,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 524 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(525); +var parse = __webpack_require__(530); var types = parse.types; module.exports = function (re, opts) { @@ -62091,13 +62258,13 @@ function isRegExp (x) { /***/ }), -/* 525 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(526); -var types = __webpack_require__(527); -var sets = __webpack_require__(528); -var positions = __webpack_require__(529); +var util = __webpack_require__(531); +var types = __webpack_require__(532); +var sets = __webpack_require__(533); +var positions = __webpack_require__(534); module.exports = function(regexpStr) { @@ -62379,11 +62546,11 @@ module.exports.types = types; /***/ }), -/* 526 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(527); -var sets = __webpack_require__(528); +var types = __webpack_require__(532); +var sets = __webpack_require__(533); // All of these are private and only used by randexp. @@ -62496,7 +62663,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 527 */ +/* 532 */ /***/ (function(module, exports) { module.exports = { @@ -62512,10 +62679,10 @@ module.exports = { /***/ }), -/* 528 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(527); +var types = __webpack_require__(532); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -62600,10 +62767,10 @@ exports.anyChar = function() { /***/ }), -/* 529 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(527); +var types = __webpack_require__(532); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -62623,7 +62790,7 @@ exports.end = function() { /***/ }), -/* 530 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62636,8 +62803,8 @@ exports.end = function() { -var isobject = __webpack_require__(531); -var isDescriptor = __webpack_require__(532); +var isobject = __webpack_require__(536); +var isDescriptor = __webpack_require__(537); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -62668,7 +62835,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 531 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62687,7 +62854,7 @@ module.exports = function isObject(val) { /***/ }), -/* 532 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62700,9 +62867,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(533); -var isAccessor = __webpack_require__(534); -var isData = __webpack_require__(536); +var typeOf = __webpack_require__(538); +var isAccessor = __webpack_require__(539); +var isData = __webpack_require__(541); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -62716,7 +62883,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 533 */ +/* 538 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -62851,7 +63018,7 @@ function isBuffer(val) { /***/ }), -/* 534 */ +/* 539 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62864,7 +63031,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(535); +var typeOf = __webpack_require__(540); // accessor descriptor properties var accessor = { @@ -62927,7 +63094,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 535 */ +/* 540 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63062,7 +63229,7 @@ function isBuffer(val) { /***/ }), -/* 536 */ +/* 541 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63075,7 +63242,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(537); +var typeOf = __webpack_require__(542); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -63118,7 +63285,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 537 */ +/* 542 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63253,14 +63420,14 @@ function isBuffer(val) { /***/ }), -/* 538 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(539); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(544); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63320,7 +63487,7 @@ function isEnum(obj, key) { /***/ }), -/* 539 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63333,7 +63500,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63341,7 +63508,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 540 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63354,7 +63521,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(531); +var isObject = __webpack_require__(536); function isObjectObject(o) { return isObject(o) === true @@ -63385,7 +63552,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 541 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63432,14 +63599,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 542 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(543); -var safe = __webpack_require__(524); +var extend = __webpack_require__(548); +var safe = __webpack_require__(529); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -63511,14 +63678,14 @@ module.exports = toRegex; /***/ }), -/* 543 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(544); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(549); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63578,7 +63745,7 @@ function isEnum(obj, key) { /***/ }), -/* 544 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63591,7 +63758,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63599,7 +63766,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 545 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63649,13 +63816,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 546 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(547); +var isObject = __webpack_require__(552); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -63689,7 +63856,7 @@ function hasOwn(obj, key) { /***/ }), -/* 547 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63709,13 +63876,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 548 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(549); +var utils = __webpack_require__(554); module.exports = function(braces, options) { braces.compiler @@ -63998,25 +64165,25 @@ function hasQueue(node) { /***/ }), -/* 549 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(550); +var splitString = __webpack_require__(555); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(546); -utils.flatten = __webpack_require__(553); -utils.isObject = __webpack_require__(531); -utils.fillRange = __webpack_require__(554); -utils.repeat = __webpack_require__(560); -utils.unique = __webpack_require__(545); +utils.extend = __webpack_require__(551); +utils.flatten = __webpack_require__(558); +utils.isObject = __webpack_require__(536); +utils.fillRange = __webpack_require__(559); +utils.repeat = __webpack_require__(565); +utils.unique = __webpack_require__(550); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -64348,7 +64515,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 550 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64361,7 +64528,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(551); +var extend = __webpack_require__(556); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -64526,14 +64693,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 551 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(552); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(557); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -64593,7 +64760,7 @@ function isEnum(obj, key) { /***/ }), -/* 552 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64606,7 +64773,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -64614,7 +64781,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 553 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64643,7 +64810,7 @@ function flat(arr, res) { /***/ }), -/* 554 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64657,10 +64824,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(555); -var extend = __webpack_require__(546); -var repeat = __webpack_require__(558); -var toRegex = __webpack_require__(559); +var isNumber = __webpack_require__(560); +var extend = __webpack_require__(551); +var repeat = __webpack_require__(563); +var toRegex = __webpack_require__(564); /** * Return a range of numbers or letters. @@ -64858,7 +65025,7 @@ module.exports = fillRange; /***/ }), -/* 555 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64871,7 +65038,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(556); +var typeOf = __webpack_require__(561); module.exports = function isNumber(num) { var type = typeOf(num); @@ -64887,10 +65054,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 556 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -65009,7 +65176,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 557 */ +/* 562 */ /***/ (function(module, exports) { /*! @@ -65036,7 +65203,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 558 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65113,7 +65280,7 @@ function repeat(str, num) { /***/ }), -/* 559 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65126,8 +65293,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(558); -var isNumber = __webpack_require__(555); +var repeat = __webpack_require__(563); +var isNumber = __webpack_require__(560); var cache = {}; function toRegexRange(min, max, options) { @@ -65414,7 +65581,7 @@ module.exports = toRegexRange; /***/ }), -/* 560 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65439,14 +65606,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 561 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(562); -var utils = __webpack_require__(549); +var Node = __webpack_require__(567); +var utils = __webpack_require__(554); /** * Braces parsers @@ -65806,15 +65973,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 562 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(531); -var define = __webpack_require__(563); -var utils = __webpack_require__(564); +var isObject = __webpack_require__(536); +var define = __webpack_require__(568); +var utils = __webpack_require__(569); var ownNames; /** @@ -66305,7 +66472,7 @@ exports = module.exports = Node; /***/ }), -/* 563 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66318,7 +66485,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(532); +var isDescriptor = __webpack_require__(537); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66343,13 +66510,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 564 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(556); +var typeOf = __webpack_require__(561); var utils = module.exports; /** @@ -67369,17 +67536,17 @@ function assert(val, message) { /***/ }), -/* 565 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(546); -var Snapdragon = __webpack_require__(566); -var compilers = __webpack_require__(548); -var parsers = __webpack_require__(561); -var utils = __webpack_require__(549); +var extend = __webpack_require__(551); +var Snapdragon = __webpack_require__(571); +var compilers = __webpack_require__(553); +var parsers = __webpack_require__(566); +var utils = __webpack_require__(554); /** * Customize Snapdragon parser and renderer @@ -67480,17 +67647,17 @@ module.exports = Braces; /***/ }), -/* 566 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(567); -var define = __webpack_require__(594); -var Compiler = __webpack_require__(604); -var Parser = __webpack_require__(633); -var utils = __webpack_require__(613); +var Base = __webpack_require__(572); +var define = __webpack_require__(599); +var Compiler = __webpack_require__(609); +var Parser = __webpack_require__(638); +var utils = __webpack_require__(618); var regexCache = {}; var cache = {}; @@ -67661,20 +67828,20 @@ module.exports.Parser = Parser; /***/ }), -/* 567 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(568); -var CacheBase = __webpack_require__(569); -var Emitter = __webpack_require__(570); -var isObject = __webpack_require__(531); -var merge = __webpack_require__(588); -var pascal = __webpack_require__(591); -var cu = __webpack_require__(592); +var define = __webpack_require__(573); +var CacheBase = __webpack_require__(574); +var Emitter = __webpack_require__(575); +var isObject = __webpack_require__(536); +var merge = __webpack_require__(593); +var pascal = __webpack_require__(596); +var cu = __webpack_require__(597); /** * Optionally define a custom `cache` namespace to use. @@ -68103,7 +68270,7 @@ module.exports.namespace = namespace; /***/ }), -/* 568 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68116,7 +68283,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(532); +var isDescriptor = __webpack_require__(537); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -68141,21 +68308,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 569 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(531); -var Emitter = __webpack_require__(570); -var visit = __webpack_require__(571); -var toPath = __webpack_require__(574); -var union = __webpack_require__(575); -var del = __webpack_require__(579); -var get = __webpack_require__(577); -var has = __webpack_require__(584); -var set = __webpack_require__(587); +var isObject = __webpack_require__(536); +var Emitter = __webpack_require__(575); +var visit = __webpack_require__(576); +var toPath = __webpack_require__(579); +var union = __webpack_require__(580); +var del = __webpack_require__(584); +var get = __webpack_require__(582); +var has = __webpack_require__(589); +var set = __webpack_require__(592); /** * Create a `Cache` constructor that when instantiated will @@ -68409,7 +68576,7 @@ module.exports.namespace = namespace; /***/ }), -/* 570 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { @@ -68578,7 +68745,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 571 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68591,8 +68758,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(572); -var mapVisit = __webpack_require__(573); +var visit = __webpack_require__(577); +var mapVisit = __webpack_require__(578); module.exports = function(collection, method, val) { var result; @@ -68615,7 +68782,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 572 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68628,7 +68795,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(531); +var isObject = __webpack_require__(536); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -68655,14 +68822,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 573 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(572); +var visit = __webpack_require__(577); /** * Map `visit` over an array of objects. @@ -68699,7 +68866,7 @@ function isObject(val) { /***/ }), -/* 574 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68712,7 +68879,7 @@ function isObject(val) { -var typeOf = __webpack_require__(556); +var typeOf = __webpack_require__(561); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -68739,16 +68906,16 @@ function filter(arr) { /***/ }), -/* 575 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(547); -var union = __webpack_require__(576); -var get = __webpack_require__(577); -var set = __webpack_require__(578); +var isObject = __webpack_require__(552); +var union = __webpack_require__(581); +var get = __webpack_require__(582); +var set = __webpack_require__(583); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -68776,7 +68943,7 @@ function arrayify(val) { /***/ }), -/* 576 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68812,7 +68979,7 @@ module.exports = function union(init) { /***/ }), -/* 577 */ +/* 582 */ /***/ (function(module, exports) { /*! @@ -68868,7 +69035,7 @@ function toString(val) { /***/ }), -/* 578 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68881,10 +69048,10 @@ function toString(val) { -var split = __webpack_require__(550); -var extend = __webpack_require__(546); -var isPlainObject = __webpack_require__(540); -var isObject = __webpack_require__(547); +var split = __webpack_require__(555); +var extend = __webpack_require__(551); +var isPlainObject = __webpack_require__(545); +var isObject = __webpack_require__(552); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -68930,7 +69097,7 @@ function isValidKey(key) { /***/ }), -/* 579 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68943,8 +69110,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(531); -var has = __webpack_require__(580); +var isObject = __webpack_require__(536); +var has = __webpack_require__(585); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -68969,7 +69136,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 580 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68982,9 +69149,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(581); -var hasValues = __webpack_require__(583); -var get = __webpack_require__(577); +var isObject = __webpack_require__(586); +var hasValues = __webpack_require__(588); +var get = __webpack_require__(582); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -68995,7 +69162,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 581 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69008,7 +69175,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(582); +var isArray = __webpack_require__(587); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69016,7 +69183,7 @@ module.exports = function isObject(val) { /***/ }), -/* 582 */ +/* 587 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69027,7 +69194,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 583 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69070,7 +69237,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 584 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69083,9 +69250,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(531); -var hasValues = __webpack_require__(585); -var get = __webpack_require__(577); +var isObject = __webpack_require__(536); +var hasValues = __webpack_require__(590); +var get = __webpack_require__(582); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69093,7 +69260,7 @@ module.exports = function(val, prop) { /***/ }), -/* 585 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69106,8 +69273,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(586); -var isNumber = __webpack_require__(555); +var typeOf = __webpack_require__(591); +var isNumber = __webpack_require__(560); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -69160,10 +69327,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 586 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -69285,7 +69452,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 587 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69298,10 +69465,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(550); -var extend = __webpack_require__(546); -var isPlainObject = __webpack_require__(540); -var isObject = __webpack_require__(547); +var split = __webpack_require__(555); +var extend = __webpack_require__(551); +var isPlainObject = __webpack_require__(545); +var isObject = __webpack_require__(552); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69347,14 +69514,14 @@ function isValidKey(key) { /***/ }), -/* 588 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(589); -var forIn = __webpack_require__(590); +var isExtendable = __webpack_require__(594); +var forIn = __webpack_require__(595); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69418,7 +69585,7 @@ module.exports = mixinDeep; /***/ }), -/* 589 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69431,7 +69598,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -69439,7 +69606,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 590 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69462,7 +69629,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 591 */ +/* 596 */ /***/ (function(module, exports) { /*! @@ -69489,14 +69656,14 @@ module.exports = pascalcase; /***/ }), -/* 592 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(593); +var utils = __webpack_require__(598); /** * Expose class utils @@ -69861,7 +70028,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 593 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69875,10 +70042,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(576); -utils.define = __webpack_require__(594); -utils.isObj = __webpack_require__(531); -utils.staticExtend = __webpack_require__(601); +utils.union = __webpack_require__(581); +utils.define = __webpack_require__(599); +utils.isObj = __webpack_require__(536); +utils.staticExtend = __webpack_require__(606); /** @@ -69889,7 +70056,7 @@ module.exports = utils; /***/ }), -/* 594 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69902,7 +70069,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(595); +var isDescriptor = __webpack_require__(600); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -69927,7 +70094,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 595 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69940,9 +70107,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(596); -var isAccessor = __webpack_require__(597); -var isData = __webpack_require__(599); +var typeOf = __webpack_require__(601); +var isAccessor = __webpack_require__(602); +var isData = __webpack_require__(604); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -69956,7 +70123,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 596 */ +/* 601 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70109,7 +70276,7 @@ function isBuffer(val) { /***/ }), -/* 597 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70122,7 +70289,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(598); +var typeOf = __webpack_require__(603); // accessor descriptor properties var accessor = { @@ -70185,10 +70352,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 598 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -70307,7 +70474,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 599 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70320,7 +70487,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(600); +var typeOf = __webpack_require__(605); // data descriptor properties var data = { @@ -70369,10 +70536,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 600 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -70491,7 +70658,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 601 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70504,8 +70671,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(602); -var define = __webpack_require__(594); +var copy = __webpack_require__(607); +var define = __webpack_require__(599); var util = __webpack_require__(112); /** @@ -70588,15 +70755,15 @@ module.exports = extend; /***/ }), -/* 602 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(556); -var copyDescriptor = __webpack_require__(603); -var define = __webpack_require__(594); +var typeOf = __webpack_require__(561); +var copyDescriptor = __webpack_require__(608); +var define = __webpack_require__(599); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -70769,7 +70936,7 @@ module.exports.has = has; /***/ }), -/* 603 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70857,16 +71024,16 @@ function isObject(val) { /***/ }), -/* 604 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(605); -var define = __webpack_require__(594); -var debug = __webpack_require__(607)('snapdragon:compiler'); -var utils = __webpack_require__(613); +var use = __webpack_require__(610); +var define = __webpack_require__(599); +var debug = __webpack_require__(612)('snapdragon:compiler'); +var utils = __webpack_require__(618); /** * Create a new `Compiler` with the given `options`. @@ -71020,7 +71187,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(632); + var sourcemaps = __webpack_require__(637); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -71041,7 +71208,7 @@ module.exports = Compiler; /***/ }), -/* 605 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71054,7 +71221,7 @@ module.exports = Compiler; -var utils = __webpack_require__(606); +var utils = __webpack_require__(611); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71169,7 +71336,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 606 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71183,8 +71350,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(594); -utils.isObject = __webpack_require__(531); +utils.define = __webpack_require__(599); +utils.isObject = __webpack_require__(536); utils.isString = function(val) { @@ -71199,7 +71366,7 @@ module.exports = utils; /***/ }), -/* 607 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71208,14 +71375,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(608); + module.exports = __webpack_require__(613); } else { - module.exports = __webpack_require__(611); + module.exports = __webpack_require__(616); } /***/ }), -/* 608 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71224,7 +71391,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(609); +exports = module.exports = __webpack_require__(614); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71406,7 +71573,7 @@ function localstorage() { /***/ }), -/* 609 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { @@ -71422,7 +71589,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(610); +exports.humanize = __webpack_require__(615); /** * The currently active debug mode names, and names to skip. @@ -71614,7 +71781,7 @@ function coerce(val) { /***/ }), -/* 610 */ +/* 615 */ /***/ (function(module, exports) { /** @@ -71772,7 +71939,7 @@ function plural(ms, n, name) { /***/ }), -/* 611 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71788,7 +71955,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(609); +exports = module.exports = __webpack_require__(614); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -71967,7 +72134,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(612); + var net = __webpack_require__(617); stream = new net.Socket({ fd: fd, readable: false, @@ -72026,13 +72193,13 @@ exports.enable(load()); /***/ }), -/* 612 */ +/* 617 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 613 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72042,9 +72209,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(546); -exports.SourceMap = __webpack_require__(614); -exports.sourceMapResolve = __webpack_require__(625); +exports.extend = __webpack_require__(551); +exports.SourceMap = __webpack_require__(619); +exports.sourceMapResolve = __webpack_require__(630); /** * Convert backslash in the given string to forward slashes @@ -72087,7 +72254,7 @@ exports.last = function(arr, n) { /***/ }), -/* 614 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72095,13 +72262,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__(615).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(621).SourceMapConsumer; -exports.SourceNode = __webpack_require__(624).SourceNode; +exports.SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(626).SourceMapConsumer; +exports.SourceNode = __webpack_require__(629).SourceNode; /***/ }), -/* 615 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72111,10 +72278,10 @@ exports.SourceNode = __webpack_require__(624).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(616); -var util = __webpack_require__(618); -var ArraySet = __webpack_require__(619).ArraySet; -var MappingList = __webpack_require__(620).MappingList; +var base64VLQ = __webpack_require__(621); +var util = __webpack_require__(623); +var ArraySet = __webpack_require__(624).ArraySet; +var MappingList = __webpack_require__(625).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72523,7 +72690,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 616 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72563,7 +72730,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(617); +var base64 = __webpack_require__(622); // 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, @@ -72669,7 +72836,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 617 */ +/* 622 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72742,7 +72909,7 @@ exports.decode = function (charCode) { /***/ }), -/* 618 */ +/* 623 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73165,7 +73332,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 619 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73175,7 +73342,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(618); +var util = __webpack_require__(623); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73292,7 +73459,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 620 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73302,7 +73469,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(618); +var util = __webpack_require__(623); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73377,7 +73544,7 @@ exports.MappingList = MappingList; /***/ }), -/* 621 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73387,11 +73554,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(618); -var binarySearch = __webpack_require__(622); -var ArraySet = __webpack_require__(619).ArraySet; -var base64VLQ = __webpack_require__(616); -var quickSort = __webpack_require__(623).quickSort; +var util = __webpack_require__(623); +var binarySearch = __webpack_require__(627); +var ArraySet = __webpack_require__(624).ArraySet; +var base64VLQ = __webpack_require__(621); +var quickSort = __webpack_require__(628).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74465,7 +74632,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 622 */ +/* 627 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74582,7 +74749,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 623 */ +/* 628 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74702,7 +74869,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 624 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74712,8 +74879,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(615).SourceMapGenerator; -var util = __webpack_require__(618); +var SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; +var util = __webpack_require__(623); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75121,17 +75288,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 625 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(626) -var resolveUrl = __webpack_require__(627) -var decodeUriComponent = __webpack_require__(628) -var urix = __webpack_require__(630) -var atob = __webpack_require__(631) +var sourceMappingURL = __webpack_require__(631) +var resolveUrl = __webpack_require__(632) +var decodeUriComponent = __webpack_require__(633) +var urix = __webpack_require__(635) +var atob = __webpack_require__(636) @@ -75429,7 +75596,7 @@ module.exports = { /***/ }), -/* 626 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75492,7 +75659,7 @@ void (function(root, factory) { /***/ }), -/* 627 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75510,13 +75677,13 @@ module.exports = resolveUrl /***/ }), -/* 628 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(629) +var decodeUriComponent = __webpack_require__(634) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75527,7 +75694,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 629 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75628,7 +75795,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 630 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75651,7 +75818,7 @@ module.exports = urix /***/ }), -/* 631 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75665,7 +75832,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 632 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75673,8 +75840,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(594); -var utils = __webpack_require__(613); +var define = __webpack_require__(599); +var utils = __webpack_require__(618); /** * Expose `mixin()`. @@ -75817,19 +75984,19 @@ exports.comment = function(node) { /***/ }), -/* 633 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(605); +var use = __webpack_require__(610); var util = __webpack_require__(112); -var Cache = __webpack_require__(634); -var define = __webpack_require__(594); -var debug = __webpack_require__(607)('snapdragon:parser'); -var Position = __webpack_require__(635); -var utils = __webpack_require__(613); +var Cache = __webpack_require__(639); +var define = __webpack_require__(599); +var debug = __webpack_require__(612)('snapdragon:parser'); +var Position = __webpack_require__(640); +var utils = __webpack_require__(618); /** * Create a new `Parser` with the given `input` and `options`. @@ -76357,7 +76524,7 @@ module.exports = Parser; /***/ }), -/* 634 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76464,13 +76631,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 635 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(594); +var define = __webpack_require__(599); /** * Store position for a node @@ -76485,14 +76652,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 636 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(637); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(642); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -76552,7 +76719,7 @@ function isEnum(obj, key) { /***/ }), -/* 637 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76565,7 +76732,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -76573,14 +76740,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 638 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(639); -var extglob = __webpack_require__(654); +var nanomatch = __webpack_require__(644); +var extglob = __webpack_require__(659); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76657,7 +76824,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 639 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76668,17 +76835,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(523); -var extend = __webpack_require__(640); +var toRegex = __webpack_require__(528); +var extend = __webpack_require__(645); /** * Local dependencies */ -var compilers = __webpack_require__(642); -var parsers = __webpack_require__(643); -var cache = __webpack_require__(646); -var utils = __webpack_require__(648); +var compilers = __webpack_require__(647); +var parsers = __webpack_require__(648); +var cache = __webpack_require__(651); +var utils = __webpack_require__(653); var MAX_LENGTH = 1024 * 64; /** @@ -77502,14 +77669,14 @@ module.exports = nanomatch; /***/ }), -/* 640 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(641); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(646); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77569,7 +77736,7 @@ function isEnum(obj, key) { /***/ }), -/* 641 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77582,7 +77749,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -77590,7 +77757,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 642 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77936,15 +78103,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 643 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(542); -var toRegex = __webpack_require__(523); -var isOdd = __webpack_require__(644); +var regexNot = __webpack_require__(547); +var toRegex = __webpack_require__(528); +var isOdd = __webpack_require__(649); /** * Characters to use in negation regex (we want to "not" match @@ -78330,7 +78497,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 644 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78343,7 +78510,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(645); +var isNumber = __webpack_require__(650); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78357,7 +78524,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 645 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78385,14 +78552,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 646 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(647))(); +module.exports = new (__webpack_require__(652))(); /***/ }), -/* 647 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78405,7 +78572,7 @@ module.exports = new (__webpack_require__(647))(); -var MapCache = __webpack_require__(634); +var MapCache = __webpack_require__(639); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78527,7 +78694,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 648 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78540,14 +78707,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(649)(); -var Snapdragon = __webpack_require__(566); -utils.define = __webpack_require__(650); -utils.diff = __webpack_require__(651); -utils.extend = __webpack_require__(640); -utils.pick = __webpack_require__(652); -utils.typeOf = __webpack_require__(653); -utils.unique = __webpack_require__(545); +var isWindows = __webpack_require__(654)(); +var Snapdragon = __webpack_require__(571); +utils.define = __webpack_require__(655); +utils.diff = __webpack_require__(656); +utils.extend = __webpack_require__(645); +utils.pick = __webpack_require__(657); +utils.typeOf = __webpack_require__(658); +utils.unique = __webpack_require__(550); /** * Returns true if the given value is effectively an empty string @@ -78913,7 +79080,7 @@ utils.unixify = function(options) { /***/ }), -/* 649 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -78941,7 +79108,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 650 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78954,8 +79121,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(531); -var isDescriptor = __webpack_require__(532); +var isobject = __webpack_require__(536); +var isDescriptor = __webpack_require__(537); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -78986,7 +79153,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 651 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79040,7 +79207,7 @@ function diffArray(one, two) { /***/ }), -/* 652 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79053,7 +79220,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(531); +var isObject = __webpack_require__(536); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -79082,7 +79249,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 653 */ +/* 658 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79217,7 +79384,7 @@ function isBuffer(val) { /***/ }), -/* 654 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79227,18 +79394,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(546); -var unique = __webpack_require__(545); -var toRegex = __webpack_require__(523); +var extend = __webpack_require__(551); +var unique = __webpack_require__(550); +var toRegex = __webpack_require__(528); /** * Local dependencies */ -var compilers = __webpack_require__(655); -var parsers = __webpack_require__(661); -var Extglob = __webpack_require__(664); -var utils = __webpack_require__(663); +var compilers = __webpack_require__(660); +var parsers = __webpack_require__(666); +var Extglob = __webpack_require__(669); +var utils = __webpack_require__(668); var MAX_LENGTH = 1024 * 64; /** @@ -79555,13 +79722,13 @@ module.exports = extglob; /***/ }), -/* 655 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(656); +var brackets = __webpack_require__(661); /** * Extglob compilers @@ -79731,7 +79898,7 @@ module.exports = function(extglob) { /***/ }), -/* 656 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79741,17 +79908,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(657); -var parsers = __webpack_require__(659); +var compilers = __webpack_require__(662); +var parsers = __webpack_require__(664); /** * Module dependencies */ -var debug = __webpack_require__(607)('expand-brackets'); -var extend = __webpack_require__(546); -var Snapdragon = __webpack_require__(566); -var toRegex = __webpack_require__(523); +var debug = __webpack_require__(612)('expand-brackets'); +var extend = __webpack_require__(551); +var Snapdragon = __webpack_require__(571); +var toRegex = __webpack_require__(528); /** * Parses the given POSIX character class `pattern` and returns a @@ -79949,13 +80116,13 @@ module.exports = brackets; /***/ }), -/* 657 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(658); +var posix = __webpack_require__(663); module.exports = function(brackets) { brackets.compiler @@ -80043,7 +80210,7 @@ module.exports = function(brackets) { /***/ }), -/* 658 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80072,14 +80239,14 @@ module.exports = { /***/ }), -/* 659 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(660); -var define = __webpack_require__(594); +var utils = __webpack_require__(665); +var define = __webpack_require__(599); /** * Text regex @@ -80298,14 +80465,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 660 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(523); -var regexNot = __webpack_require__(542); +var toRegex = __webpack_require__(528); +var regexNot = __webpack_require__(547); var cached; /** @@ -80339,15 +80506,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 661 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(656); -var define = __webpack_require__(662); -var utils = __webpack_require__(663); +var brackets = __webpack_require__(661); +var define = __webpack_require__(667); +var utils = __webpack_require__(668); /** * Characters to use in text regex (we want to "not" match @@ -80502,7 +80669,7 @@ module.exports = parsers; /***/ }), -/* 662 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80515,7 +80682,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(532); +var isDescriptor = __webpack_require__(537); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -80540,14 +80707,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 663 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(542); -var Cache = __webpack_require__(647); +var regex = __webpack_require__(547); +var Cache = __webpack_require__(652); /** * Utils @@ -80616,7 +80783,7 @@ utils.createRegex = function(str) { /***/ }), -/* 664 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80626,16 +80793,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(566); -var define = __webpack_require__(662); -var extend = __webpack_require__(546); +var Snapdragon = __webpack_require__(571); +var define = __webpack_require__(667); +var extend = __webpack_require__(551); /** * Local dependencies */ -var compilers = __webpack_require__(655); -var parsers = __webpack_require__(661); +var compilers = __webpack_require__(660); +var parsers = __webpack_require__(666); /** * Customize Snapdragon parser and renderer @@ -80701,16 +80868,16 @@ module.exports = Extglob; /***/ }), -/* 665 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(654); -var nanomatch = __webpack_require__(639); -var regexNot = __webpack_require__(542); -var toRegex = __webpack_require__(523); +var extglob = __webpack_require__(659); +var nanomatch = __webpack_require__(644); +var regexNot = __webpack_require__(547); +var toRegex = __webpack_require__(528); var not; /** @@ -80791,14 +80958,14 @@ function textRegex(pattern) { /***/ }), -/* 666 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(647))(); +module.exports = new (__webpack_require__(652))(); /***/ }), -/* 667 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80811,13 +80978,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(566); -utils.define = __webpack_require__(668); -utils.diff = __webpack_require__(651); -utils.extend = __webpack_require__(636); -utils.pick = __webpack_require__(652); -utils.typeOf = __webpack_require__(669); -utils.unique = __webpack_require__(545); +var Snapdragon = __webpack_require__(571); +utils.define = __webpack_require__(673); +utils.diff = __webpack_require__(656); +utils.extend = __webpack_require__(641); +utils.pick = __webpack_require__(657); +utils.typeOf = __webpack_require__(674); +utils.unique = __webpack_require__(550); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -81114,7 +81281,7 @@ utils.unixify = function(options) { /***/ }), -/* 668 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81127,8 +81294,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(531); -var isDescriptor = __webpack_require__(532); +var isobject = __webpack_require__(536); +var isDescriptor = __webpack_require__(537); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -81159,7 +81326,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 669 */ +/* 674 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81294,7 +81461,7 @@ function isBuffer(val) { /***/ }), -/* 670 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81313,9 +81480,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(671); -var reader_1 = __webpack_require__(684); -var fs_stream_1 = __webpack_require__(688); +var readdir = __webpack_require__(676); +var reader_1 = __webpack_require__(689); +var fs_stream_1 = __webpack_require__(693); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81376,15 +81543,15 @@ exports.default = ReaderAsync; /***/ }), -/* 671 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(672); -const readdirAsync = __webpack_require__(680); -const readdirStream = __webpack_require__(683); +const readdirSync = __webpack_require__(677); +const readdirAsync = __webpack_require__(685); +const readdirStream = __webpack_require__(688); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81468,7 +81635,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 672 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81476,11 +81643,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(673); +const DirectoryReader = __webpack_require__(678); let syncFacade = { - fs: __webpack_require__(678), - forEach: __webpack_require__(679), + fs: __webpack_require__(683), + forEach: __webpack_require__(684), sync: true }; @@ -81509,7 +81676,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 673 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81518,9 +81685,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__(674); -const stat = __webpack_require__(676); -const call = __webpack_require__(677); +const normalizeOptions = __webpack_require__(679); +const stat = __webpack_require__(681); +const call = __webpack_require__(682); /** * Asynchronously reads the contents of a directory and streams the results @@ -81896,14 +82063,14 @@ module.exports = DirectoryReader; /***/ }), -/* 674 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(675); +const globToRegExp = __webpack_require__(680); module.exports = normalizeOptions; @@ -82080,7 +82247,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 675 */ +/* 680 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82217,13 +82384,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 676 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(677); +const call = __webpack_require__(682); module.exports = stat; @@ -82298,7 +82465,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 677 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82359,14 +82526,14 @@ function callOnce (fn) { /***/ }), -/* 678 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(677); +const call = __webpack_require__(682); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82430,7 +82597,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 679 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82459,7 +82626,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 680 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82467,12 +82634,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(681); -const DirectoryReader = __webpack_require__(673); +const maybe = __webpack_require__(686); +const DirectoryReader = __webpack_require__(678); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(682), + forEach: __webpack_require__(687), async: true }; @@ -82514,7 +82681,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 681 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82541,7 +82708,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 682 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82577,7 +82744,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 683 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82585,11 +82752,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(673); +const DirectoryReader = __webpack_require__(678); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(682), + forEach: __webpack_require__(687), async: true }; @@ -82609,16 +82776,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 684 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(685); -var entry_1 = __webpack_require__(687); -var pathUtil = __webpack_require__(686); +var deep_1 = __webpack_require__(690); +var entry_1 = __webpack_require__(692); +var pathUtil = __webpack_require__(691); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82684,14 +82851,14 @@ exports.default = Reader; /***/ }), -/* 685 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(686); -var patternUtils = __webpack_require__(517); +var pathUtils = __webpack_require__(691); +var patternUtils = __webpack_require__(522); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -82774,7 +82941,7 @@ exports.default = DeepFilter; /***/ }), -/* 686 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82805,14 +82972,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 687 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(686); -var patternUtils = __webpack_require__(517); +var pathUtils = __webpack_require__(691); +var patternUtils = __webpack_require__(522); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -82897,7 +83064,7 @@ exports.default = EntryFilter; /***/ }), -/* 688 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82917,8 +83084,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(689); -var fs_1 = __webpack_require__(693); +var fsStat = __webpack_require__(694); +var fs_1 = __webpack_require__(698); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -82968,14 +83135,14 @@ exports.default = FileSystemStream; /***/ }), -/* 689 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(690); -const statProvider = __webpack_require__(692); +const optionsManager = __webpack_require__(695); +const statProvider = __webpack_require__(697); /** * Asynchronous API. */ @@ -83006,13 +83173,13 @@ exports.statSync = statSync; /***/ }), -/* 690 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(691); +const fsAdapter = __webpack_require__(696); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83025,7 +83192,7 @@ exports.prepare = prepare; /***/ }), -/* 691 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83048,7 +83215,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 692 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83100,7 +83267,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 693 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83131,7 +83298,7 @@ exports.default = FileSystem; /***/ }), -/* 694 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83151,9 +83318,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(671); -var reader_1 = __webpack_require__(684); -var fs_stream_1 = __webpack_require__(688); +var readdir = __webpack_require__(676); +var reader_1 = __webpack_require__(689); +var fs_stream_1 = __webpack_require__(693); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83221,7 +83388,7 @@ exports.default = ReaderStream; /***/ }), -/* 695 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83240,9 +83407,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(671); -var reader_1 = __webpack_require__(684); -var fs_sync_1 = __webpack_require__(696); +var readdir = __webpack_require__(676); +var reader_1 = __webpack_require__(689); +var fs_sync_1 = __webpack_require__(701); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83302,7 +83469,7 @@ exports.default = ReaderSync; /***/ }), -/* 696 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83321,8 +83488,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(689); -var fs_1 = __webpack_require__(693); +var fsStat = __webpack_require__(694); +var fs_1 = __webpack_require__(698); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83368,7 +83535,7 @@ exports.default = FileSystemSync; /***/ }), -/* 697 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83384,7 +83551,7 @@ exports.flatten = flatten; /***/ }), -/* 698 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83405,13 +83572,13 @@ exports.merge = merge; /***/ }), -/* 699 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(700); +const pathType = __webpack_require__(705); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83477,13 +83644,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 700 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(701); +const pify = __webpack_require__(706); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83526,7 +83693,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 701 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83617,17 +83784,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 702 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(513); -const gitIgnore = __webpack_require__(703); -const pify = __webpack_require__(704); -const slash = __webpack_require__(705); +const fastGlob = __webpack_require__(518); +const gitIgnore = __webpack_require__(708); +const pify = __webpack_require__(709); +const slash = __webpack_require__(710); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -83725,7 +83892,7 @@ module.exports.sync = options => { /***/ }), -/* 703 */ +/* 708 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84194,7 +84361,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 704 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84269,7 +84436,7 @@ module.exports = (input, options) => { /***/ }), -/* 705 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84287,7 +84454,7 @@ module.exports = input => { /***/ }), -/* 706 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84300,7 +84467,7 @@ module.exports = input => { -var isGlob = __webpack_require__(707); +var isGlob = __webpack_require__(712); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84320,7 +84487,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 707 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84351,17 +84518,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 708 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(709); -const CpFileError = __webpack_require__(712); -const fs = __webpack_require__(714); -const ProgressEmitter = __webpack_require__(717); +const pEvent = __webpack_require__(714); +const CpFileError = __webpack_require__(717); +const fs = __webpack_require__(719); +const ProgressEmitter = __webpack_require__(722); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84475,12 +84642,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 709 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(710); +const pTimeout = __webpack_require__(715); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -84771,12 +84938,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 710 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(711); +const pFinally = __webpack_require__(716); class TimeoutError extends Error { constructor(message) { @@ -84822,7 +84989,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 711 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84844,12 +85011,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 712 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(713); +const NestedError = __webpack_require__(718); class CpFileError extends NestedError { constructor(message, nested) { @@ -84863,7 +85030,7 @@ module.exports = CpFileError; /***/ }), -/* 713 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -84919,16 +85086,16 @@ module.exports = NestedError; /***/ }), -/* 714 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(715); -const pEvent = __webpack_require__(709); -const CpFileError = __webpack_require__(712); +const makeDir = __webpack_require__(720); +const pEvent = __webpack_require__(714); +const CpFileError = __webpack_require__(717); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85025,7 +85192,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 715 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85033,7 +85200,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__(716); +const semver = __webpack_require__(721); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85188,7 +85355,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 716 */ +/* 721 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -86790,7 +86957,7 @@ function coerce (version, options) { /***/ }), -/* 717 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86831,7 +86998,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 718 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86877,12 +87044,12 @@ exports.default = module.exports; /***/ }), -/* 719 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(720); +const pMap = __webpack_require__(725); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -86899,7 +87066,7 @@ module.exports.default = pFilter; /***/ }), -/* 720 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86978,12 +87145,12 @@ module.exports.default = pMap; /***/ }), -/* 721 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(713); +const NestedError = __webpack_require__(718); class CpyError extends NestedError { constructor(message, nested) { diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 4a85eca206c96..f899a5b44ab6c 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -108,4 +108,14 @@ module.exports = { '[/\\\\]node_modules(?![\\/\\\\]monaco-editor)[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', ], + + // An array of regexp pattern strings that are matched against all source file paths, matched files to include/exclude for code coverage + collectCoverageFrom: [ + '**/*.{js,mjs,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,mocks,tests,test_helpers,integration_tests,types}/**/*', + '!**/*mock*.ts', + '!**/*.test.ts', + '!**/*.d.ts', + '!**/index.{js,ts}', + ], }; diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts index 5bbc72fe04e86..910c9ad246700 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts @@ -19,7 +19,7 @@ import dedent from 'dedent'; -import { createFailureIssue, updateFailureIssue } from './report_failure'; +import { createFailureIssue, getCiType, updateFailureIssue } from './report_failure'; jest.mock('./github_api'); const { GithubApi } = jest.requireMock('./github_api'); @@ -51,7 +51,7 @@ describe('createFailureIssue()', () => { this is the failure text \`\`\` - First failure: [Jenkins Build](https://build-url) + First failure: [${getCiType()} Build](https://build-url) ", Array [ @@ -111,7 +111,7 @@ describe('updateFailureIssue()', () => { "calls": Array [ Array [ 1234, - "New failure: [Jenkins Build](https://build-url)", + "New failure: [${getCiType()} Build](https://build-url)", ], ], "results": Array [ diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts index 1413d05498459..30ec6ab939560 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts @@ -21,6 +21,10 @@ import { TestFailure } from './get_failures'; import { GithubIssueMini, GithubApi } from './github_api'; import { getIssueMetadata, updateIssueMetadata } from './issue_metadata'; +export function getCiType() { + return process.env.TEAMCITY_CI ? 'TeamCity' : 'Jenkins'; +} + export async function createFailureIssue(buildUrl: string, failure: TestFailure, api: GithubApi) { const title = `Failing test: ${failure.classname} - ${failure.name}`; @@ -32,7 +36,7 @@ export async function createFailureIssue(buildUrl: string, failure: TestFailure, failure.failure, '```', '', - `First failure: [Jenkins Build](${buildUrl})`, + `First failure: [${getCiType()} Build](${buildUrl})`, ].join('\n'), { 'test.class': failure.classname, @@ -52,7 +56,7 @@ export async function updateFailureIssue(buildUrl: string, issue: GithubIssueMin }); await api.editIssueBodyAndEnsureOpen(issue.number, newBody); - await api.addIssueComment(issue.number, `New failure: [Jenkins Build](${buildUrl})`); + await api.addIssueComment(issue.number, `New failure: [${getCiType()} Build](${buildUrl})`); return newCount; } diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 93616ce78a04a..9010e324bb392 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -33,6 +33,17 @@ import { getReportMessageIter } from './report_metadata'; const DEFAULT_PATTERNS = [Path.resolve(REPO_ROOT, 'target/junit/**/*.xml')]; +const getBranch = () => { + if (process.env.TEAMCITY_CI) { + return (process.env.GIT_BRANCH || '').replace(/^refs\/heads\//, ''); + } else { + // JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others + const jobNameSplit = (process.env.JOB_NAME || '').split(/\+|\//); + const branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH; + return branch; + } +}; + export function runFailedTestsReporterCli() { run( async ({ log, flags }) => { @@ -44,16 +55,15 @@ export function runFailedTestsReporterCli() { } if (updateGithub) { - // JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others - const jobNameSplit = (process.env.JOB_NAME || '').split(/\+|\//); - const branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH; + const branch = getBranch(); if (!branch) { throw createFailError( 'Unable to determine originating branch from job name or other environment variables' ); } - const isPr = !!process.env.ghprbPullId; + // ghprbPullId check can be removed once there are no PR jobs running on Jenkins + const isPr = !!process.env.GITHUB_PR_NUMBER || !!process.env.ghprbPullId; const isMasterOrVersion = branch === 'master' || branch.match(/^\d+\.(x|\d+)$/); if (!isMasterOrVersion || isPr) { log.info('Failure issues only created on master/version branch jobs'); @@ -69,7 +79,9 @@ export function runFailedTestsReporterCli() { const buildUrl = flags['build-url'] || (updateGithub ? '' : 'http://buildUrl'); if (typeof buildUrl !== 'string' || !buildUrl) { - throw createFlagError('Missing --build-url or process.env.BUILD_URL'); + throw createFlagError( + 'Missing --build-url, process.env.TEAMCITY_BUILD_URL, or process.env.BUILD_URL' + ); } const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS; @@ -161,12 +173,12 @@ export function runFailedTestsReporterCli() { default: { 'github-update': true, 'report-update': true, - 'build-url': process.env.BUILD_URL, + 'build-url': process.env.TEAMCITY_BUILD_URL || process.env.BUILD_URL, }, help: ` --no-github-update Execute the CLI without writing to Github --no-report-update Execute the CLI without writing to the JUnit reports - --build-url URL of the failed build, defaults to process.env.BUILD_URL + --build-url URL of the failed build, defaults to process.env.TEAMCITY_BUILD_URL or process.env.BUILD_URL `, }, } diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 407ab37123d5d..605ad38efbc96 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -67,6 +67,7 @@ describe('dev/mocha/junit report generation', () => { expect(testsuite).to.eql({ $: { failures: '2', + name: 'test', skipped: '1', tests: '4', time: testsuite.$.time, diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 84d488bd8b5a1..de28fceb967e2 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -108,6 +108,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { ); const testsuitesEl = builder.ele('testsuite', { + name: reportName, timestamp: new Date(stats.startTime).toISOString().slice(0, -5), time: getDuration(stats), tests: allTests.length + failedHooks.length, diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts index 697e5bc37d602..c4dca1b84f4eb 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts @@ -17,10 +17,10 @@ * under the License. */ -jest.mock('../legacy_logging_server'); +jest.mock('@kbn/legacy-logging'); import { LogRecord, LogLevel } from '../../../logging'; -import { LegacyLoggingServer } from '../legacy_logging_server'; +import { LegacyLoggingServer } from '@kbn/legacy-logging'; import { LegacyAppender } from './legacy_appender'; afterEach(() => (LegacyLoggingServer as any).mockClear()); diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index 67337c7d67629..286448231d23f 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -18,8 +18,8 @@ */ import { schema } from '@kbn/config-schema'; -import { DisposableAppender, LogRecord } from '../../../logging'; -import { LegacyLoggingServer } from '../legacy_logging_server'; +import { LegacyLoggingServer } from '@kbn/legacy-logging'; +import { DisposableAppender, LogRecord } from '@kbn/logging'; import { LegacyVars } from '../../types'; export interface LegacyAppenderConfig { diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index afe58ddff92aa..2fca2f35cb032 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -25,7 +25,7 @@ jest.mock('fs', () => ({ const dynamicProps = { process: { pid: expect.any(Number) } }; -jest.mock('../../../legacy/server/logging/rotate', () => ({ +jest.mock('@kbn/legacy-logging', () => ({ setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), })); diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 654c3f9948a18..93d7218b11c28 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -39,17 +39,5 @@ export default { '/test/functional/services/remote', '/src/dev/code_coverage/ingest_coverage', ], - collectCoverageFrom: [ - 'src/plugins/**/*.{ts,tsx}', - '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*', - '!src/plugins/**/*.d.ts', - '!src/plugins/**/test_helpers/**', - 'packages/kbn-ui-framework/src/components/**/*.js', - '!packages/kbn-ui-framework/src/components/index.js', - '!packages/kbn-ui-framework/src/components/**/*/index.js', - 'packages/kbn-ui-framework/src/services/**/*.js', - '!packages/kbn-ui-framework/src/services/index.js', - '!packages/kbn-ui-framework/src/services/**/*/index.js', - ], testRunner: 'jasmine2', }; diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index d859c7e45fa20..8448d20aa2fc8 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -70,8 +70,11 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/plugins/apm/e2e/**/*', 'x-pack/plugins/maps/server/fonts/**/*', + // packages for the ingest manager's api integration tests could be valid semver which has dashes 'x-pack/test/fleet_api_integration/apis/fixtures/test_packages/**/*', + + '.teamcity/**/*', ]; /** diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 39df3990ff2ff..a9b5eec45a75b 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -19,6 +19,7 @@ import Joi from 'joi'; import os from 'os'; +import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; const HANDLED_IN_NEW_PLATFORM = Joi.any().description( 'This key is handled in the new platform ONLY' @@ -77,51 +78,7 @@ export default () => uiSettings: HANDLED_IN_NEW_PLATFORM, - logging: Joi.object() - .keys({ - appenders: HANDLED_IN_NEW_PLATFORM, - loggers: HANDLED_IN_NEW_PLATFORM, - root: HANDLED_IN_NEW_PLATFORM, - - silent: Joi.boolean().default(false), - - quiet: Joi.boolean().when('silent', { - is: true, - then: Joi.default(true).valid(true), - otherwise: Joi.default(false), - }), - - verbose: Joi.boolean().when('quiet', { - is: true, - then: Joi.valid(false).default(false), - otherwise: Joi.default(false), - }), - events: Joi.any().default({}), - dest: Joi.string().default('stdout'), - filter: Joi.any().default({}), - json: Joi.boolean().when('dest', { - is: 'stdout', - then: Joi.default(!process.stdout.isTTY), - otherwise: Joi.default(true), - }), - timezone: Joi.string(), - rotate: Joi.object() - .keys({ - enabled: Joi.boolean().default(false), - everyBytes: Joi.number() - // > 1MB - .greater(1048576) - // < 1GB - .less(1073741825) - // 10MB - .default(10485760), - keepFiles: Joi.number().greater(2).less(1024).default(7), - pollingInterval: Joi.number().greater(5000).less(3600000).default(10000), - usePolling: Joi.boolean().default(false), - }) - .default(), - }) - .default(), + logging: legacyLoggingConfigSchema, ops: Joi.object({ interval: Joi.number().default(5000), diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 013da35d2acb7..b61a86326ca1a 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -18,12 +18,12 @@ */ import { constant, once, compact, flatten } from 'lodash'; +import { reconfigureLogging } from '@kbn/legacy-logging'; import { isWorker } from 'cluster'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { fromRoot, pkg } from '../../core/server/utils'; import { Config } from './config'; -import loggingConfiguration from './logging/configuration'; import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; @@ -154,13 +154,17 @@ export default class KbnServer { applyLoggingConfiguration(settings) { const config = Config.withDefaultSchema(settings); - const loggingOptions = loggingConfiguration(config); + + const loggingConfig = config.get('logging'); + const opsConfig = config.get('ops'); + const subset = { - ops: config.get('ops'), - logging: config.get('logging'), + ops: opsConfig, + logging: loggingConfig, }; const plain = JSON.stringify(subset, null, 2); this.server.log(['info', 'config'], 'New logging configuration:\n' + plain); - this.server.plugins['@elastic/good'].reconfigure(loggingOptions); + + reconfigureLogging(this.server, loggingConfig, opsConfig.interval); } } diff --git a/src/legacy/server/logging/index.js b/src/legacy/server/logging/index.js index 5182de0b7f613..cb252ba37dc4e 100644 --- a/src/legacy/server/logging/index.js +++ b/src/legacy/server/logging/index.js @@ -17,21 +17,16 @@ * under the License. */ -import good from '@elastic/good'; -import loggingConfiguration from './configuration'; -import { logWithMetadata } from './log_with_metadata'; -import { setupLoggingRotate } from './rotate'; +import { setupLogging, setupLoggingRotate, attachMetaData } from '@kbn/legacy-logging'; -export async function setupLogging(server, config) { - return await server.register({ - plugin: good, - options: loggingConfiguration(config), +export async function loggingMixin(kbnServer, server, config) { + server.decorate('server', 'logWithMetadata', (tags, message, metadata = {}) => { + server.log(tags, attachMetaData(message, metadata)); }); -} -export async function loggingMixin(kbnServer, server, config) { - logWithMetadata.decorateServer(server); + const loggingConfig = config.get('logging'); + const opsInterval = config.get('ops.interval'); - await setupLogging(server, config); - await setupLoggingRotate(server, config); + await setupLogging(server, loggingConfig, opsInterval); + await setupLoggingRotate(server, loggingConfig); } diff --git a/src/plugins/charts/server/plugin.ts b/src/plugins/charts/server/plugin.ts index 0123459bd25d2..2a9b82afefc98 100644 --- a/src/plugins/charts/server/plugin.ts +++ b/src/plugins/charts/server/plugin.ts @@ -41,8 +41,18 @@ export class ChartsServerPlugin implements Plugin { }), type: 'json', description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', { - defaultMessage: 'Maps values to specified colors within visualizations', + defaultMessage: + 'Maps values to specific colors in Visualize charts and TSVB. This setting does not apply to Lens.', }), + deprecation: { + message: i18n.translate( + 'charts.advancedSettings.visualization.colorMappingTextDeprecation', + { + defaultMessage: 'This setting is deprecated and will not be supported as of 8.0.', + } + ), + docLinksKey: 'visualizationSettings', + }, category: ['visualization'], schema: schema.string(), }, diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index afaa2d00d8cfd..3e09fa449a1aa 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -47,7 +47,7 @@ Object { ], }, "count": 1, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 850c5a312fda1..4dd2d29f38e9f 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -68,12 +68,12 @@ export class IndexPatternField implements IFieldType { this.spec.lang = lang; } - public get customName() { - return this.spec.customName; + public get customLabel() { + return this.spec.customLabel; } - public set customName(label) { - this.spec.customName = label; + public set customLabel(customLabel) { + this.spec.customLabel = customLabel; } /** @@ -93,8 +93,8 @@ export class IndexPatternField implements IFieldType { } public get displayName(): string { - return this.spec.customName - ? this.spec.customName + return this.spec.customLabel + ? this.spec.customLabel : this.spec.shortDotsEnable ? shortenDottedString(this.spec.name) : this.spec.name; @@ -163,7 +163,7 @@ export class IndexPatternField implements IFieldType { aggregatable: this.aggregatable, readFromDocValues: this.readFromDocValues, subType: this.subType, - customName: this.customName, + customLabel: this.customLabel, }; } @@ -186,7 +186,7 @@ export class IndexPatternField implements IFieldType { readFromDocValues: this.readFromDocValues, subType: this.subType, format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, - customName: this.customName, + customLabel: this.customLabel, shortDotsEnable: this.spec.shortDotsEnable, }; } diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 86c22b0116ead..1c70a2e884025 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -37,7 +37,7 @@ export interface IFieldType { scripted?: boolean; subType?: IFieldSubType; displayName?: string; - customName?: string; + customLabel?: string; format?: any; toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 2741322acec0f..e2bdb0009c20a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -9,7 +9,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -33,7 +33,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], @@ -57,7 +57,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "_id", ], @@ -81,7 +81,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "_source", ], @@ -105,7 +105,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "_type", ], @@ -129,7 +129,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "geo_shape", ], @@ -153,7 +153,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 10, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "long", ], @@ -177,7 +177,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "conflict", ], @@ -201,7 +201,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -225,7 +225,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -253,7 +253,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "geo_point", ], @@ -277,7 +277,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -301,7 +301,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "murmur3", ], @@ -325,7 +325,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "ip", ], @@ -349,7 +349,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -373,7 +373,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -401,7 +401,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -425,7 +425,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -449,7 +449,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "integer", ], @@ -473,7 +473,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "geo_point", ], @@ -497,7 +497,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "attachment", ], @@ -521,7 +521,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], @@ -545,7 +545,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "murmur3", ], @@ -569,7 +569,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "long", ], @@ -593,7 +593,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -617,7 +617,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 20, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "boolean", ], @@ -641,7 +641,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], @@ -665,7 +665,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index c3a0c98745e21..47ad5860801bc 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -135,8 +135,8 @@ export class IndexPattern implements IIndexPattern { const newFieldAttrs = { ...this.fieldAttrs }; this.fields.forEach((field) => { - if (field.customName) { - newFieldAttrs[field.name] = { customName: field.customName }; + if (field.customLabel) { + newFieldAttrs[field.name] = { customLabel: field.customLabel }; } else { delete newFieldAttrs[field.name]; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index d51de220111e3..82c8cf4abc5ac 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -309,7 +309,7 @@ export class IndexPatternsService { */ fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => fields.reduce((collector, field) => { - collector[field.name] = { ...field, customName: fieldAttrs?.[field.name]?.customName }; + collector[field.name] = { ...field, customLabel: fieldAttrs?.[field.name]?.customLabel }; return collector; }, {}); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 22c400562f6d4..28b077f4bfdf3 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -52,7 +52,7 @@ export interface IndexPatternAttributes { } export interface FieldAttrs { - [key: string]: { customName: string }; + [key: string]: { customLabel: string }; } export type OnNotification = (toastInputFields: ToastInputFields) => void; @@ -169,7 +169,7 @@ export interface FieldSpec { readFromDocValues?: boolean; subType?: IFieldSubType; indexed?: boolean; - customName?: string; + customLabel?: string; // not persisted shortDotsEnable?: boolean; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6c4609e5506c2..fc9b8d4839ea3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -978,7 +978,7 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) - customName?: string; + customLabel?: string; // (undocumented) displayName?: string; // (undocumented) @@ -1152,7 +1152,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; // (undocumented) @@ -1259,8 +1259,8 @@ export class IndexPatternField implements IFieldType { get count(): number; set count(count: number); // (undocumented) - get customName(): string | undefined; - set customName(label: string | undefined); + get customLabel(): string | undefined; + set customLabel(customLabel: string | undefined); // (undocumented) get displayName(): string; // (undocumented) @@ -1299,7 +1299,7 @@ export class IndexPatternField implements IFieldType { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - customName: string | undefined; + customLabel: string | undefined; }; // (undocumented) toSpec({ getFormatterForField, }?: { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 8d1699c4ad5ed..47e17c26398d3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -507,7 +507,7 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) - customName?: string; + customLabel?: string; // (undocumented) displayName?: string; // (undocumented) @@ -612,7 +612,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; // (undocumented) diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 4b63eb5c56fd1..8dd95adf00cc8 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -151,9 +151,9 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); -const customNameDescription = i18n.translate( - 'indexPatternManagement.editIndexPattern.fields.table.customNameTooltip', - { defaultMessage: 'A custom name for the field.' } +const labelDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip', + { defaultMessage: 'A custom label for the field.' } ); interface IndexedFieldProps { @@ -197,11 +197,11 @@ export class Table extends PureComponent { /> ) : null} - {field.customName && field.customName !== field.name ? ( + {field.customLabel && field.customLabel !== field.name ? (
- + - {field.customName} + {field.customLabel}
diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index babfbbfc2a763..29cbec38a5982 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -54,15 +54,15 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` } - label="Custom name" + label="Custom label" > @@ -294,15 +294,15 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` } - label="Custom name" + label="Custom label" > } - label="Custom name" + label="Custom label" > } - label="Custom name" + label="Custom label" > } - label="Custom name" + label="Custom label" > { expect(component).toMatchSnapshot(); }); - it('should display and update a customName correctly', async () => { + it('should display and update a custom label correctly', async () => { let testField = ({ name: 'test', format: new Format(), lang: undefined, type: 'string', - customName: 'Test', + customLabel: 'Test', } as unknown) as IndexPatternField; fieldList.push(testField); indexPattern.fields.getByName = (name) => { @@ -219,14 +219,14 @@ describe('FieldEditor', () => { await new Promise((resolve) => process.nextTick(resolve)); component.update(); - const input = findTestSubject(component, 'editorFieldCustomName'); + const input = findTestSubject(component, 'editorFieldCustomLabel'); expect(input.props().value).toBe('Test'); input.simulate('change', { target: { value: 'new Test' } }); const saveBtn = findTestSubject(component, 'fieldSaveButton'); await saveBtn.simulate('click'); await new Promise((resolve) => process.nextTick(resolve)); - expect(testField.customName).toEqual('new Test'); + expect(testField.customLabel).toEqual('new Test'); }); it('should show deprecated lang warning', async () => { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 97d30d88e018c..29a87a65fdff7 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -126,7 +126,7 @@ export interface FieldEditorState { errors?: string[]; format: any; spec: IndexPatternField['spec']; - customName: string; + customLabel: string; } export interface FieldEdiorProps { @@ -167,7 +167,7 @@ export class FieldEditor extends PureComponent } > { - this.setState({ customName: e.target.value }); + this.setState({ customLabel: e.target.value }); }} /> @@ -802,7 +802,7 @@ export class FieldEditor extends PureComponent { const field = this.state.spec; const { indexPattern } = this.props; - const { fieldFormatId, fieldFormatParams, customName } = this.state; + const { fieldFormatId, fieldFormatParams, customLabel } = this.state; if (field.scripted) { this.setState({ @@ -843,8 +843,8 @@ export class FieldEditor extends PureComponent {this.renderScriptingPanels()} {this.renderName()} - {this.renderCustomName()} + {this.renderCustomLabel()} {this.renderLanguage()} {this.renderType()} {this.renderTypeConflict()} diff --git a/src/plugins/vis_type_timeseries/common/metric_types.js b/src/plugins/vis_type_timeseries/common/metric_types.ts similarity index 63% rename from src/plugins/vis_type_timeseries/common/metric_types.js rename to src/plugins/vis_type_timeseries/common/metric_types.ts index 05836a6df410a..a045dbf38c1f9 100644 --- a/src/plugins/vis_type_timeseries/common/metric_types.js +++ b/src/plugins/vis_type_timeseries/common/metric_types.ts @@ -17,20 +17,26 @@ * under the License. */ -export const METRIC_TYPES = { - PERCENTILE: 'percentile', - PERCENTILE_RANK: 'percentile_rank', - TOP_HIT: 'top_hit', - COUNT: 'count', - DERIVATIVE: 'derivative', - STD_DEVIATION: 'std_deviation', - VARIANCE: 'variance', - SUM_OF_SQUARES: 'sum_of_squares', - CARDINALITY: 'cardinality', - VALUE_COUNT: 'value_count', - AVERAGE: 'avg', - SUM: 'sum', -}; +// We should probably use METRIC_TYPES from data plugin in future. +export enum METRIC_TYPES { + PERCENTILE = 'percentile', + PERCENTILE_RANK = 'percentile_rank', + TOP_HIT = 'top_hit', + COUNT = 'count', + DERIVATIVE = 'derivative', + STD_DEVIATION = 'std_deviation', + VARIANCE = 'variance', + SUM_OF_SQUARES = 'sum_of_squares', + CARDINALITY = 'cardinality', + VALUE_COUNT = 'value_count', + AVERAGE = 'avg', + SUM = 'sum', +} + +// We should probably use BUCKET_TYPES from data plugin in future. +export enum BUCKET_TYPES { + TERMS = 'terms', +} export const EXTENDED_STATS_TYPES = [ METRIC_TYPES.STD_DEVIATION, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js index 83ddc23648ad3..feda9fd239a66 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js @@ -23,10 +23,10 @@ import { includes } from 'lodash'; import { injectI18n } from '@kbn/i18n/react'; import { EuiComboBox } from '@elastic/eui'; import { calculateSiblings } from '../lib/calculate_siblings'; -import { calculateLabel } from '../../../../../../plugins/vis_type_timeseries/common/calculate_label'; -import { basicAggs } from '../../../../../../plugins/vis_type_timeseries/common/basic_aggs'; -import { toPercentileNumber } from '../../../../../../plugins/vis_type_timeseries/common/to_percentile_number'; -import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types'; +import { calculateLabel } from '../../../../common/calculate_label'; +import { basicAggs } from '../../../../common/basic_aggs'; +import { toPercentileNumber } from '../../../../common/to_percentile_number'; +import { METRIC_TYPES } from '../../../../common/metric_types'; function createTypeFilter(restrict, exclude) { return (metric) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index fb945d2606bc8..48b6f6192a93c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -37,7 +37,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { MODEL_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/model_options'; +import { MODEL_TYPES } from '../../../../common/model_options'; const DEFAULTS = { model_type: MODEL_TYPES.UNWEIGHTED, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index c63beee222b17..1969147efde9a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -36,7 +36,7 @@ import { } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; -import { PANEL_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../../common/panel_types'; const isFieldTypeEnabled = (fieldRestrictions, fieldType) => fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index 30c6d5b51d187..85f31285df69b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -42,11 +42,8 @@ import { AUTO_INTERVAL, } from './lib/get_interval'; import { i18n } from '@kbn/i18n'; -import { - TIME_RANGE_DATA_MODES, - TIME_RANGE_MODE_KEY, -} from '../../../../../plugins/vis_type_timeseries/common/timerange_data_modes'; -import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/timerange_data_modes'; +import { PANEL_TYPES } from '../../../common/panel_types'; import { isTimerangeModeEnabled } from '../lib/check_ui_restrictions'; import { VisDataContext } from '../contexts/vis_data_context'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 0f64c570088d7..66783f5ef2715 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -19,7 +19,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../common/get_last_value'; import { createTickFormatter } from './tick_formatter'; import { labelDateFormatter } from './label_date_formatter'; import moment from 'moment'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js index 86361afca3b12..c1d484765f4cb 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { search } from '../../../../../../plugins/data/public'; const { parseEsInterval } = search.aggs; -import { GTE_INTERVAL_RE } from '../../../../../../plugins/vis_type_timeseries/common/interval_regexp'; +import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; export const AUTO_INTERVAL = 'auto'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js index 146e7a4bae15a..f8b6f19ac21a2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js @@ -18,7 +18,7 @@ */ import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; -import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types'; +import { METRIC_TYPES } from '../../../../common/metric_types'; export function getSupportedFieldsByMetricType(type) { switch (type) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js index 0638c6e67f5ef..b6b99d7782762 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import { newMetricAggFn } from './new_metric_agg_fn'; -import { isBasicAgg } from '../../../../../../plugins/vis_type_timeseries/common/agg_lookup'; +import { isBasicAgg } from '../../../../common/agg_lookup'; import { handleAdd, handleChange } from './collection_actions'; export const seriesChangeHandler = (props, items) => (doc) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js index bb3f0041abca7..b2ea90d6a87fe 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js @@ -45,7 +45,10 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +import { VisDataContext } from './../../contexts/vis_data_context'; +import { BUCKET_TYPES } from '../../../../common/metric_types'; export class TablePanelConfig extends Component { + static contextType = VisDataContext; constructor(props) { super(props); this.state = { selectedTab: 'data' }; @@ -120,6 +123,8 @@ export class TablePanelConfig extends Component { value={model.pivot_id} indexPattern={model.index_pattern} onChange={this.handlePivotChange} + uiRestrictions={this.context.uiRestrictions} + type={BUCKET_TYPES.TERMS} fullWidth /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index a72c7598509a8..fe6c89ea6985b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -36,7 +36,7 @@ import { EuiFieldText, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { FIELD_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/field_types'; +import { FIELD_TYPES } from '../../../../common/field_types'; import { STACKED_OPTIONS } from '../../visualizations/constants'; const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 47b30f9ab2711..57adecd9d598b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -28,7 +28,7 @@ import { VisPicker } from './vis_picker'; import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; -import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; +import { extractIndexPatterns } from '../../../common/extract_index_patterns'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js index 9c2b947bda08e..9742d817f7c0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js @@ -28,7 +28,7 @@ import { isGteInterval, AUTO_INTERVAL, } from './lib/get_interval'; -import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../common/panel_types'; const MIN_CHART_HEIGHT = 300; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js index c33ed02eadebd..79f5c7abca270 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { EuiTabs, EuiTab } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; -import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../common/panel_types'; function VisPickerItem(props) { const { label, type, selected } = props; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js index 4c029f1c0d5b0..325e9c8372736 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js @@ -23,7 +23,7 @@ import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; import _, { get, isUndefined, assign, includes } from 'lodash'; import { Gauge } from '../../../visualizations/views/gauge'; -import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../../common/get_last_value'; function getColors(props) { const { model, visData } = props; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index f37971e990c96..5fe7afe47df9b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -23,7 +23,7 @@ import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; import _, { get, isUndefined, assign, includes, pick } from 'lodash'; import { Metric } from '../../../visualizations/views/metric'; -import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; function getColors(props) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js index b44c94131348d..099dbe6639737 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js @@ -17,7 +17,7 @@ * under the License. */ -import { basicAggs } from '../../../../../../../plugins/vis_type_timeseries/common/basic_aggs'; +import { basicAggs } from '../../../../../common/basic_aggs'; export function isSortable(metric) { return basicAggs.includes(metric.type); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index 1341cf02202a0..92109e1a37426 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -22,7 +22,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { RedirectAppLinks } from '../../../../../../kibana_react/public'; import { createTickFormatter } from '../../lib/tick_formatter'; -import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label'; +import { calculateLabel } from '../../../../../common/calculate_label'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; @@ -30,7 +30,7 @@ import { fieldFormats } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { getFieldFormats, getCoreStart } from '../../../../services'; -import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; +import { METRIC_TYPES } from '../../../../../common/metric_types'; function getColor(rules, colorKey, value) { let color; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js index 680c1c5e78ad4..039763efc78a2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js @@ -35,7 +35,7 @@ import { import { Split } from '../../split'; import { createTextHandler } from '../../lib/create_text_handler'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { PANEL_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../../../common/panel_types'; const TimeseriesSeriesUI = injectI18n(function (props) { const { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index e9f64c93d337f..1c2ebb8264ef3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -20,7 +20,7 @@ import { getCoreStart } from '../../../../services'; import { createTickFormatter } from '../../lib/tick_formatter'; import { TopN } from '../../../visualizations/views/top_n'; -import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index f583d087e60ef..27891cdbb3943 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -21,7 +21,7 @@ import React from 'react'; import { getDisplayName } from './lib/get_display_name'; import { labelDateFormatter } from './lib/label_date_formatter'; import { last, findIndex, first } from 'lodash'; -import { calculateLabel } from '../../../../../plugins/vis_type_timeseries/common/calculate_label'; +import { calculateLabel } from '../../../common/calculate_label'; export function visWithSplits(WrappedComponent) { function SplitVisComponent(props) { diff --git a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js index 5d18c0a2f09cd..d77f2f327b30d 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js @@ -18,10 +18,7 @@ */ import { get } from 'lodash'; -import { - RESTRICTIONS_KEYS, - DEFAULT_UI_RESTRICTION, -} from '../../../../../plugins/vis_type_timeseries/common/ui_restrictions'; +import { RESTRICTIONS_KEYS, DEFAULT_UI_RESTRICTION } from '../../../common/ui_restrictions'; /** * Generic method for checking all types of the UI Restrictions diff --git a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js index e8ddb4ceb5cba..9448a29787097 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js @@ -17,7 +17,7 @@ * under the License. */ -import { GTE_INTERVAL_RE } from '../../../../../plugins/vis_type_timeseries/common/interval_regexp'; +import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; import { i18n } from '@kbn/i18n'; import { search } from '../../../../../plugins/data/public'; const { parseInterval } = search.aggs; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js index 50a2042425438..0b9e191e4e29e 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js @@ -22,7 +22,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../common/get_last_value'; import { getValueBy } from '../lib/get_value_by'; import { GaugeVis } from './gauge_vis'; import reactcss from 'reactcss'; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js index 4c286f61720ac..7356726e6262f 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js @@ -20,8 +20,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; import reactcss from 'reactcss'; + +import { getLastValue } from '../../../../common/get_last_value'; import { calculateCoordinates } from '../lib/calculate_coordinates'; export class Metric extends Component { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js index 136ac2506d392..9c6e497b92dab 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../common/get_last_value'; import { labelDateFormatter } from '../../components/lib/label_date_formatter'; import reactcss from 'reactcss'; diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json index 0f9820a6c2f6e..0f2edc8c510c3 100644 --- a/test/functional/fixtures/es_archiver/discover/data.json +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -8,7 +8,7 @@ "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", "timeFieldName": "@timestamp", "title": "logstash-*", - "fieldAttrs": "{\"referer\":{\"customName\":\"Referer custom\"}}" + "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}" }, "type": "index-pattern" } diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index c57cdb40ae952..56397351562de 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -9,7 +9,7 @@ "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "timeFieldName": "@timestamp", "title": "logstash-*", - "fieldAttrs": "{\"utc_time\":{\"customName\":\"UTC time\"}}" + "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}" }, "type": "index-pattern" } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index b3b7fd32eae19..8f03b1d760258 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -191,6 +191,18 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { return await driver.get(url); } + /** + * Retrieves the cookie with the given name. Returns null if there is no such cookie. The cookie will be returned as + * a JSON object as described by the WebDriver wire protocol. + * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Options.html + * + * @param {string} cookieName + * @return {Promise} + */ + public async getCookie(cookieName: string) { + return await driver.manage().getCookie(cookieName); + } + /** * Pauses the execution in the browser, similar to setting a breakpoint for debugging. * @return {Promise} diff --git a/tsconfig.json b/tsconfig.json index 88ae3e1e826b3..6e137e445762d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,7 @@ { "path": "./src/core/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, + { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, @@ -39,6 +40,7 @@ { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/url_forwarding/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./src/test_utils/tsconfig.json" } ] diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index dd9cc21954e61..40cc298db795a 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -8,6 +8,15 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; // always firing export const DEFAULT_INSTANCES_TO_GENERATE = 5; +export interface AlwaysFiringParams { + instances?: number; + thresholds?: { + small?: number; + medium?: number; + large?: number; + }; +} +export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds']; // Astros export enum Craft { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index a5d158fca836b..abbe1d2a48d11 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -4,17 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldNumber, + EuiFormRow, + EuiPopover, + EuiExpression, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; -import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; - -interface AlwaysFiringParamsProps { - alertParams: { instances?: number }; - setAlertParams: (property: string, value: any) => void; - errors: { [key: string]: string[] }; -} +import { omit, pick } from 'lodash'; +import { + ActionGroupWithCondition, + AlertConditions, + AlertConditionsGroup, + AlertTypeModel, + AlertTypeParamsExpressionProps, + AlertsContextValue, +} from '../../../../plugins/triggers_actions_ui/public'; +import { + AlwaysFiringParams, + AlwaysFiringActionGroupIds, + DEFAULT_INSTANCES_TO_GENERATE, +} from '../../common/constants'; export function getAlertType(): AlertTypeModel { return { @@ -24,7 +38,7 @@ export function getAlertType(): AlertTypeModel { iconClass: 'bolt', documentationUrl: null, alertParamsExpression: AlwaysFiringExpression, - validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => { + validate: (alertParams: AlwaysFiringParams) => { const { instances } = alertParams; const validationResult = { errors: { @@ -44,11 +58,30 @@ export function getAlertType(): AlertTypeModel { }; } -export const AlwaysFiringExpression: React.FunctionComponent = ({ - alertParams, - setAlertParams, -}) => { - const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams; +const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = { + small: 0, + medium: 5000, + large: 10000, +}; + +export const AlwaysFiringExpression: React.FunctionComponent> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => { + const { + instances = DEFAULT_INSTANCES_TO_GENERATE, + thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId), + } = alertParams; + + const actionGroupsWithConditions = actionGroups.map((actionGroup) => + Number.isInteger(thresholds[actionGroup.id as AlwaysFiringActionGroupIds]) + ? { + ...actionGroup, + conditions: thresholds[actionGroup.id as AlwaysFiringActionGroupIds]!, + } + : actionGroup + ); + return ( @@ -67,6 +100,88 @@ export const AlwaysFiringExpression: React.FunctionComponent + + + + { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} + > + { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + + + + + ); }; + +interface TShirtSelectorProps { + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; +} +const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { + const [isOpen, setIsOpen] = useState(false); + + if (!actionGroup) { + return null; + } + + return ( + setIsOpen(true)} + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + ownFocus + anchorPosition="downLeft" + > + + + {'Is Above'} + + + { + const conditions = parseInt(e.target.value, 10); + if (e.target.value && !isNaN(conditions)) { + setTShirtThreshold({ + ...actionGroup, + conditions, + }); + } + }} + /> + + + + ); +}; diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index d02406a23045e..1900f55a51a55 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -5,31 +5,56 @@ */ import uuid from 'uuid'; -import { range, random } from 'lodash'; +import { range } from 'lodash'; import { AlertType } from '../../../../plugins/alerts/server'; -import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { + DEFAULT_INSTANCES_TO_GENERATE, + ALERTING_EXAMPLE_APP_ID, + AlwaysFiringParams, +} from '../../common/constants'; const ACTION_GROUPS = [ - { id: 'small', name: 'small' }, - { id: 'medium', name: 'medium' }, - { id: 'large', name: 'large' }, + { id: 'small', name: 'Small t-shirt' }, + { id: 'medium', name: 'Medium t-shirt' }, + { id: 'large', name: 'Large t-shirt' }, ]; +const DEFAULT_ACTION_GROUP = 'small'; -export const alertType: AlertType = { +function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) { + const idAsNumber = parseInt(id, 10); + if (!isNaN(idAsNumber)) { + if (thresholds?.large && thresholds.large < idAsNumber) { + return 'large'; + } + if (thresholds?.medium && thresholds.medium < idAsNumber) { + return 'medium'; + } + if (thresholds?.small && thresholds.small < idAsNumber) { + return 'small'; + } + } + return DEFAULT_ACTION_GROUP; +} + +export const alertType: AlertType = { id: 'example.always-firing', name: 'Always firing', actionGroups: ACTION_GROUPS, - defaultActionGroupId: 'small', - async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { + defaultActionGroupId: DEFAULT_ACTION_GROUP, + async executor({ + services, + params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds }, + state, + }) { const count = (state.count ?? 0) + 1; range(instances) - .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! })) - .forEach((instance: { id: string; tshirtSize: string }) => { + .map(() => uuid.v4()) + .forEach((id: string) => { services - .alertInstanceFactory(instance.id) + .alertInstanceFactory(id) .replaceState({ triggerdOnCycle: count }) - .scheduleActions(instance.tshirtSize); + .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds)); }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index a9d1e28182b29..f1c9df3b25fed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -14,7 +14,15 @@ import { actionsConfigMock } from '../actions_config.mock'; import { licenseStateMock } from '../lib/license_state.mock'; import { licensingMock } from '../../../licensing/server/mocks'; -const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook']; +const ACTION_TYPE_IDS = [ + '.index', + '.email', + '.pagerduty', + '.server-log', + '.slack', + '.teams', + '.webhook', +]; export function createActionTypeRegistry(): { logger: jest.Mocked; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 3591e05fb3acf..edbf13d9e5ed1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -17,6 +17,7 @@ import { getActionType as getWebhookActionType } from './webhook'; import { getActionType as getServiceNowActionType } from './servicenow'; import { getActionType as getJiraActionType } from './jira'; import { getActionType as getResilientActionType } from './resilient'; +import { getActionType as getTeamsActionType } from './teams'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, @@ -36,4 +37,5 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getTeamsActionType({ logger, configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts new file mode 100644 index 0000000000000..ffa7c778c0489 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from '../../../../../src/core/server'; +import { Services } from '../types'; +import { validateParams, validateSecrets } from '../lib'; +import axios from 'axios'; +import { ActionParamsType, ActionTypeSecretsType, getActionType, TeamsActionType } from './teams'; +import { actionsConfigMock } from '../actions_config.mock'; +import { actionsMock } from '../mocks'; +import { createActionTypeRegistry } from './index.test'; +import * as utils from './lib/axios_utils'; + +jest.mock('axios'); +jest.mock('./lib/axios_utils', () => { + const originalUtils = jest.requireActual('./lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +const ACTION_TYPE_ID = '.teams'; + +const services: Services = actionsMock.createServices(); + +let actionType: TeamsActionType; +let mockedLogger: jest.Mocked; + +beforeAll(() => { + const { logger, actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get<{}, ActionTypeSecretsType, ActionParamsType>(ACTION_TYPE_ID); + mockedLogger = logger; +}); + +describe('action registration', () => { + test('returns action type', () => { + expect(actionType.id).toEqual(ACTION_TYPE_ID); + expect(actionType.name).toEqual('Microsoft Teams'); + }); +}); + +describe('validateParams()', () => { + test('should validate and pass when params is valid', () => { + expect(validateParams(actionType, { message: 'a message' })).toEqual({ + message: 'a message', + }); + }); + + test('should validate and throw error when params is invalid', () => { + expect(() => { + validateParams(actionType, {}); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [message]: expected value of type [string] but got [undefined]"` + ); + + expect(() => { + validateParams(actionType, { message: 1 }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [message]: expected value of type [string] but got [number]"` + ); + }); +}); + +describe('validateActionTypeSecrets()', () => { + test('should validate and pass when config is valid', () => { + validateSecrets(actionType, { + webhookUrl: 'https://example.com', + }); + }); + + test('should validate and throw error when config is invalid', () => { + expect(() => { + validateSecrets(actionType, {}); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"` + ); + + expect(() => { + validateSecrets(actionType, { webhookUrl: 1 }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"` + ); + + expect(() => { + validateSecrets(actionType, { webhookUrl: 'fee-fi-fo-fum' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: error configuring teams action: unable to parse host name from webhookUrl"` + ); + }); + + test('should validate and pass when the teams webhookUrl is added to allowedHosts', () => { + actionType = getActionType({ + logger: mockedLogger, + configurationUtilities: { + ...actionsConfigMock.create(), + ensureUriAllowed: (url) => { + expect(url).toEqual('https://outlook.office.com/'); + }, + }, + }); + + expect(validateSecrets(actionType, { webhookUrl: 'https://outlook.office.com/' })).toEqual({ + webhookUrl: 'https://outlook.office.com/', + }); + }); + + test('config validation returns an error if the specified URL isnt added to allowedHosts', () => { + actionType = getActionType({ + logger: mockedLogger, + configurationUtilities: { + ...actionsConfigMock.create(), + ensureHostnameAllowed: () => { + throw new Error(`target hostname is not added to allowedHosts`); + }, + }, + }); + + expect(() => { + validateSecrets(actionType, { webhookUrl: 'https://outlook.office.com/' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: error configuring teams action: target hostname is not added to allowedHosts"` + ); + }); +}); + +describe('execute()', () => { + beforeAll(() => { + requestMock.mockReset(); + actionType = getActionType({ + logger: mockedLogger, + configurationUtilities: actionsConfigMock.create(), + }); + }); + + beforeEach(() => { + requestMock.mockReset(); + requestMock.mockResolvedValue({ + status: 200, + statusText: '', + data: '', + headers: [], + config: {}, + }); + }); + + test('calls the mock executor with success', async () => { + const response = await actionType.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "axios": undefined, + "data": Object { + "text": "this invocation should succeed", + }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from teams action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "proxySettings": undefined, + "url": "http://example.com", + } + `); + expect(response).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "data": Object { + "text": "this invocation should succeed", + }, + "status": "ok", + } + `); + }); + + test('calls the mock executor with success proxy', async () => { + const response = await actionType.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + }, + }); + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "axios": undefined, + "data": Object { + "text": "this invocation should succeed", + }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from teams action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "proxySettings": Object { + "proxyRejectUnauthorizedCertificates": false, + "proxyUrl": "https://someproxyhost", + }, + "url": "http://example.com", + } + `); + expect(response).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "data": Object { + "text": "this invocation should succeed", + }, + "status": "ok", + } + `); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.ts new file mode 100644 index 0000000000000..e152a65217ce2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.ts @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { URL } from 'url'; +import { curry, isString } from 'lodash'; +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { i18n } from '@kbn/i18n'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { map, getOrElse } from 'fp-ts/lib/Option'; +import { Logger } from '../../../../../src/core/server'; +import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; +import { isOk, promiseResult, Result } from './lib/result_type'; +import { request } from './lib/axios_utils'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +import { ActionsConfigurationUtilities } from '../actions_config'; + +export type TeamsActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +export type TeamsActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + ActionTypeSecretsType, + ActionParamsType +>; + +// secrets definition + +export type ActionTypeSecretsType = TypeOf; + +const secretsSchemaProps = { + webhookUrl: schema.string(), +}; +const SecretsSchema = schema.object(secretsSchemaProps); + +// params definition + +export type ActionParamsType = TypeOf; + +const ParamsSchema = schema.object({ + message: schema.string({ minLength: 1 }), +}); + +// action type definition +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): TeamsActionType { + return { + id: '.teams', + minimumLicenseRequired: 'gold', + name: i18n.translate('xpack.actions.builtin.teamsTitle', { + defaultMessage: 'Microsoft Teams', + }), + validate: { + secrets: schema.object(secretsSchemaProps, { + validate: curry(validateActionTypeConfig)(configurationUtilities), + }), + params: ParamsSchema, + }, + executor: curry(teamsExecutor)({ logger }), + }; +} + +function validateActionTypeConfig( + configurationUtilities: ActionsConfigurationUtilities, + secretsObject: ActionTypeSecretsType +) { + let url: URL; + try { + url = new URL(secretsObject.webhookUrl); + } catch (err) { + return i18n.translate('xpack.actions.builtin.teams.teamsConfigurationErrorNoHostname', { + defaultMessage: 'error configuring teams action: unable to parse host name from webhookUrl', + }); + } + + try { + configurationUtilities.ensureHostnameAllowed(url.hostname); + } catch (allowListError) { + return i18n.translate('xpack.actions.builtin.teams.teamsConfigurationError', { + defaultMessage: 'error configuring teams action: {message}', + values: { + message: allowListError.message, + }, + }); + } +} + +// action executor + +async function teamsExecutor( + { logger }: { logger: Logger }, + execOptions: TeamsActionTypeExecutorOptions +): Promise> { + const actionId = execOptions.actionId; + const secrets = execOptions.secrets; + const params = execOptions.params; + const { webhookUrl } = secrets; + const { message } = params; + const data = { text: message }; + + const axiosInstance = axios.create(); + + const result: Result = await promiseResult( + request({ + axios: axiosInstance, + method: 'post', + url: webhookUrl, + logger, + data, + proxySettings: execOptions.proxySettings, + }) + ); + + if (isOk(result)) { + const { + value: { status, statusText, data: responseData, headers: responseHeaders }, + } = result; + + // Microsoft Teams connectors do not throw 429s. Rather they will return a 200 response + // with a 429 message in the response body when the rate limit is hit + // https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#rate-limiting-for-connectors + if (isString(responseData) && responseData.includes('ErrorCode:ApplicationThrottled')) { + return pipe( + getRetryAfterIntervalFromHeaders(responseHeaders), + map((retry) => retryResultSeconds(actionId, message, retry)), + getOrElse(() => retryResult(actionId, message)) + ); + } + + logger.debug(`response from teams action "${actionId}": [HTTP ${status}] ${statusText}`); + + return successResult(actionId, data); + } else { + const { error } = result; + + if (error.response) { + const { status, statusText } = error.response; + const serviceMessage = `[${status}] ${statusText}`; + logger.error(`error on ${actionId} Microsoft Teams event: ${serviceMessage}`); + + // special handling for 5xx + if (status >= 500) { + return retryResult(actionId, serviceMessage); + } + + return errorResultInvalid(actionId, serviceMessage); + } + + logger.debug(`error on ${actionId} Microsoft Teams action: unexpected error`); + return errorResultUnexpectedError(actionId); + } +} + +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { + return { status: 'ok', data, actionId }; +} + +function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { + const errMessage = i18n.translate('xpack.actions.builtin.teams.unreachableErrorMessage', { + defaultMessage: 'error posting to Microsoft Teams, unexpected error', + }); + return { + status: 'error', + message: errMessage, + actionId, + }; +} + +function errorResultInvalid( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { + const errMessage = i18n.translate('xpack.actions.builtin.teams.invalidResponseErrorMessage', { + defaultMessage: 'error posting to Microsoft Teams, invalid response', + }); + return { + status: 'error', + message: errMessage, + actionId, + serviceMessage, + }; +} + +function retryResult(actionId: string, message: string): ActionTypeExecutorResult { + const errMessage = i18n.translate( + 'xpack.actions.builtin.teams.errorPostingRetryLaterErrorMessage', + { + defaultMessage: 'error posting a Microsoft Teams message, retry later', + } + ); + return { + status: 'error', + message: errMessage, + retry: true, + actionId, + }; +} + +function retryResultSeconds( + actionId: string, + message: string, + retryAfter: number +): ActionTypeExecutorResult { + const retryEpoch = Date.now() + retryAfter * 1000; + const retry = new Date(retryEpoch); + const retryString = retry.toISOString(); + const errMessage = i18n.translate( + 'xpack.actions.builtin.teams.errorPostingRetryDateErrorMessage', + { + defaultMessage: 'error posting a Microsoft Teams message, retry at {retryString}', + values: { + retryString, + }, + } + ); + return { + status: 'error', + message: errMessage, + retry, + actionId, + serviceMessage: message, + }; +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a160735e89a93..e61936321b8e0 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { first, map } from 'rxjs/operators'; +import { first } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { Observable } from 'rxjs'; import { PluginInitializerContext, Plugin, @@ -13,7 +14,6 @@ import { CoreStart, KibanaRequest, Logger, - SharedGlobalConfig, RequestHandler, IContextProvider, ElasticsearchServiceStart, @@ -128,7 +128,6 @@ const includedHiddenTypes = [ ]; export class ActionsPlugin implements Plugin, PluginStartContract> { - private readonly kibanaIndex: Promise; private readonly config: Promise; private readonly logger: Logger; @@ -143,20 +142,14 @@ export class ActionsPlugin implements Plugin, Plugi private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; constructor(initContext: PluginInitializerContext) { this.config = initContext.config.create().pipe(first()).toPromise(); - - this.kibanaIndex = initContext.config.legacy.globalConfig$ - .pipe( - first(), - map((config: SharedGlobalConfig) => config.kibana.index) - ) - .toPromise(); - this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; + this.kibanaIndexConfig = initContext.config.legacy.globalConfig$; } public async setup( @@ -220,22 +213,26 @@ export class ActionsPlugin implements Plugin, Plugi const usageCollection = plugins.usageCollection; if (usageCollection) { - initializeActionsTelemetry( - this.telemetryLogger, - plugins.taskManager, - core, - await this.kibanaIndex + registerActionsUsageCollector( + usageCollection, + core.getStartServices().then(([_, { taskManager }]) => taskManager) ); - - core.getStartServices().then(async ([, startPlugins]) => { - registerActionsUsageCollector(usageCollection, startPlugins.taskManager); - }); } - core.http.registerRouteHandlerContext( - 'actions', - this.createRouteHandlerContext(core, await this.kibanaIndex) - ); + this.kibanaIndexConfig.subscribe((config) => { + core.http.registerRouteHandlerContext( + 'actions', + this.createRouteHandlerContext(core, config.kibana.index) + ); + if (usageCollection) { + initializeActionsTelemetry( + this.telemetryLogger, + plugins.taskManager, + core, + config.kibana.index + ); + } + }); // Routes const router = core.http.createRouter(); @@ -269,7 +266,7 @@ export class ActionsPlugin implements Plugin, Plugi actionExecutor, actionTypeRegistry, taskRunnerFactory, - kibanaIndex, + kibanaIndexConfig, isESOUsingEphemeralEncryptionKey, preconfiguredActions, instantiateAuthorization, @@ -297,10 +294,12 @@ export class ActionsPlugin implements Plugin, Plugi request ); + const kibanaIndex = (await kibanaIndexConfig.pipe(first()).toPromise()).kibana.index; + return new ActionsClient({ unsecuredSavedObjectsClient, actionTypeRegistry: actionTypeRegistry!, - defaultKibanaIndex: await kibanaIndex, + defaultKibanaIndex: kibanaIndex, scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, request, diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts index 0e6c2ff37eb02..39a61cebe92dc 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts @@ -24,7 +24,7 @@ describe('registerActionsUsageCollector', () => { it('should call registerCollector', () => { registerActionsUsageCollector( usageCollectionMock as UsageCollectionSetup, - mockTaskManagerStart + new Promise(() => mockTaskManagerStart) ); expect(usageCollectionMock.registerCollector).toHaveBeenCalledTimes(1); }); @@ -32,7 +32,7 @@ describe('registerActionsUsageCollector', () => { it('should call makeUsageCollector with type = actions', () => { registerActionsUsageCollector( usageCollectionMock as UsageCollectionSetup, - mockTaskManagerStart + new Promise(() => mockTaskManagerStart) ); expect(usageCollectionMock.makeUsageCollector).toHaveBeenCalledTimes(1); expect(usageCollectionMock.makeUsageCollector.mock.calls[0][0].type).toBe('actions'); diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts index fac57b6282c44..f86c6a40e0505 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts @@ -26,11 +26,14 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { export function createActionsUsageCollector( usageCollection: UsageCollectionSetup, - taskManager: TaskManagerStartContract + taskManager: Promise ) { return usageCollection.makeUsageCollector({ type: 'actions', - isReady: () => true, + isReady: async () => { + await taskManager; + return true; + }, schema: { count_total: { type: 'long' }, count_active_total: { type: 'long' }, @@ -79,7 +82,7 @@ async function getLatestTaskState(taskManager: TaskManagerStartContract) { export function registerActionsUsageCollector( usageCollection: UsageCollectionSetup, - taskManager: TaskManagerStartContract + taskManager: Promise ) { const collector = createActionsUsageCollector(usageCollection, taskManager); usageCollection.registerCollector(collector); diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index 97a9a58400e38..88f6090d20737 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from 'kibana/server'; +import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AlertTypeState = Record; @@ -37,6 +37,7 @@ export interface AlertExecutionStatus { } export type AlertActionParams = SavedObjectAttributes; +export type AlertActionParam = SavedObjectAttribute; export interface AlertAction { group: string; diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 99cb45130718a..4bfb44425544a 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -5,6 +5,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { first, map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { combineLatest } from 'rxjs'; import { SecurityPluginSetup } from '../../security/server'; @@ -28,7 +29,6 @@ import { SavedObjectsServiceStart, IContextProvider, RequestHandler, - SharedGlobalConfig, ElasticsearchServiceStart, ILegacyClusterClient, StatusServiceSetup, @@ -124,10 +124,10 @@ export class AlertingPlugin { private security?: SecurityPluginSetup; private readonly alertsClientFactory: AlertsClientFactory; private readonly telemetryLogger: Logger; - private readonly kibanaIndex: Promise; private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; + private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.create().pipe(first()).toPromise(); @@ -135,19 +135,14 @@ export class AlertingPlugin { this.taskRunnerFactory = new TaskRunnerFactory(); this.alertsClientFactory = new AlertsClientFactory(); this.telemetryLogger = initializerContext.logger.get('usage'); - this.kibanaIndex = initializerContext.config.legacy.globalConfig$ - .pipe( - first(), - map((config: SharedGlobalConfig) => config.kibana.index) - ) - .toPromise(); + this.kibanaIndexConfig = initializerContext.config.legacy.globalConfig$; this.kibanaVersion = initializerContext.env.packageInfo.version; } - public async setup( + public setup( core: CoreSetup, plugins: AlertingPluginsSetup - ): Promise { + ): PluginSetupContract { this.licenseState = new LicenseState(plugins.licensing.license$); this.security = plugins.security; @@ -187,15 +182,17 @@ export class AlertingPlugin { const usageCollection = plugins.usageCollection; if (usageCollection) { - initializeAlertingTelemetry( - this.telemetryLogger, - core, - plugins.taskManager, - await this.kibanaIndex + registerAlertsUsageCollector( + usageCollection, + core.getStartServices().then(([_, { taskManager }]) => taskManager) ); - - core.getStartServices().then(async ([, startPlugins]) => { - registerAlertsUsageCollector(usageCollection, startPlugins.taskManager); + this.kibanaIndexConfig.subscribe((config) => { + initializeAlertingTelemetry( + this.telemetryLogger, + core, + plugins.taskManager, + config.kibana.index + ); }); } diff --git a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts index a5f83bc393d4e..e731e3f536261 100644 --- a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts +++ b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts @@ -22,12 +22,18 @@ describe('registerAlertsUsageCollector', () => { }); it('should call registerCollector', () => { - registerAlertsUsageCollector(usageCollectionMock as UsageCollectionSetup, taskManagerStart); + registerAlertsUsageCollector( + usageCollectionMock as UsageCollectionSetup, + new Promise(() => taskManagerStart) + ); expect(usageCollectionMock.registerCollector).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = alerts', () => { - registerAlertsUsageCollector(usageCollectionMock as UsageCollectionSetup, taskManagerStart); + registerAlertsUsageCollector( + usageCollectionMock as UsageCollectionSetup, + new Promise(() => taskManagerStart) + ); expect(usageCollectionMock.makeUsageCollector).toHaveBeenCalledTimes(1); expect(usageCollectionMock.makeUsageCollector.mock.calls[0][0].type).toBe('alerts'); }); diff --git a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts index de82dd31877af..40a9983ae2786 100644 --- a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts @@ -44,11 +44,14 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { export function createAlertsUsageCollector( usageCollection: UsageCollectionSetup, - taskManager: TaskManagerStartContract + taskManager: Promise ) { return usageCollection.makeUsageCollector({ type: 'alerts', - isReady: () => true, + isReady: async () => { + await taskManager; + return true; + }, fetch: async () => { try { const doc = await getLatestTaskState(await taskManager); @@ -129,7 +132,7 @@ async function getLatestTaskState(taskManager: TaskManagerStartContract) { export function registerAlertsUsageCollector( usageCollection: UsageCollectionSetup, - taskManager: TaskManagerStartContract + taskManager: Promise ) { const collector = createAlertsUsageCollector(usageCollection, taskManager); usageCollection.registerCollector(collector); diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index c0a99e0152fa7..8e563399a0f1f 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { memoize } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { asDecimalOrInteger, asInteger } from './formatters'; +import { asDecimal, asDecimalOrInteger, asInteger } from './formatters'; import { TimeUnit } from './datetime'; import { Maybe } from '../../../typings/common'; +import { isFiniteNumber } from '../is_finite_number'; interface FormatterOptions { defaultValue?: string; @@ -99,7 +100,7 @@ function convertTo({ microseconds: Maybe; defaultValue?: string; }): ConvertedDuration { - if (microseconds == null) { + if (!isFiniteNumber(microseconds)) { return { value: defaultValue, formatted: defaultValue }; } @@ -143,6 +144,29 @@ export const getDurationFormatter: TimeFormatterBuilder = memoize( } ); +export function asTransactionRate(value: Maybe) { + if (!isFiniteNumber(value)) { + return NOT_AVAILABLE_LABEL; + } + + let displayedValue: string; + + if (value === 0) { + displayedValue = '0'; + } else if (value <= 0.1) { + displayedValue = '< 0.1'; + } else { + displayedValue = asDecimal(value); + } + + return i18n.translate('xpack.apm.transactionRateLabel', { + defaultMessage: `{value} tpm`, + values: { + value: displayedValue, + }, + }); +} + /** * Converts value and returns it formatted - 00 unit */ @@ -150,7 +174,7 @@ export function asDuration( value: Maybe, { defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} ) { - if (value == null) { + if (!isFiniteNumber(value)) { return defaultValue; } diff --git a/x-pack/plugins/apm/common/utils/formatters/formatters.ts b/x-pack/plugins/apm/common/utils/formatters/formatters.ts index d84bf86d0de2f..2314e915e3161 100644 --- a/x-pack/plugins/apm/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/common/utils/formatters/formatters.ts @@ -5,6 +5,9 @@ */ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; +import { Maybe } from '../../../typings/common'; +import { NOT_AVAILABLE_LABEL } from '../../i18n'; +import { isFiniteNumber } from '../is_finite_number'; export function asDecimal(value: number) { return numeral(value).format('0,0.0'); @@ -25,11 +28,11 @@ export function tpmUnit(type?: string) { } export function asPercent( - numerator: number, + numerator: Maybe, denominator: number | undefined, - fallbackResult = '' + fallbackResult = NOT_AVAILABLE_LABEL ) { - if (!denominator || isNaN(numerator)) { + if (!denominator || !isFiniteNumber(numerator)) { return fallbackResult; } diff --git a/x-pack/plugins/apm/common/utils/is_finite_number.ts b/x-pack/plugins/apm/common/utils/is_finite_number.ts new file mode 100644 index 0000000000000..47c4f5fdbd0ee --- /dev/null +++ b/x-pack/plugins/apm/common/utils/is_finite_number.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { 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 { + return isFinite(value); +} diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index ffd3a39e8afd1..849dd7f5c3e2d 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -29,7 +29,7 @@ module.exports = { roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], collectCoverage: true, collectCoverageFrom: [ - ...(jestConfig.collectCoverageFrom ?? []), + ...(jestConfig.collectCoverageFrom || []), '**/*.{js,mjs,jsx,ts,tsx}', '!**/*.stories.{js,mjs,ts,tsx}', '!**/dev_docs/**', diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index dfc3d6b4b9ec8..7fcbe7c518cd0 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -10,7 +10,6 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router } from 'react-router-dom'; -import 'react-vis/dist/style.css'; import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; import { KibanaContextProvider, diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx new file mode 100644 index 0000000000000..3ad71b52b6037 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ScaleType, + Chart, + LineSeries, + Axis, + CurveType, + Position, + timeFormatter, + Settings, +} from '@elastic/charts'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { + APIReturnType, + callApmApi, +} from '../../../services/rest/createCallApmApi'; +import { px } from '../../../style/variables'; +import { SignificantTermsTable } from './SignificantTermsTable'; +import { ChartContainer } from '../../shared/charts/chart_container'; + +type CorrelationsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/failed_transactions'> +>; + +type SignificantTerm = NonNullable< + CorrelationsApiResponse['significantTerms'] +>[0]; + +export function ErrorCorrelations() { + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { transactionName, transactionType, start, end } = urlParams; + + const { data, status } = useFetcher(() => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/failed_transactions', + params: { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + uiFilters: JSON.stringify(uiFilters), + fieldNames: + 'transaction.name,user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name', + }, + }, + }); + } + }, [serviceName, start, end, transactionName, transactionType, uiFilters]); + + return ( + <> + + + +

Error rate over time

+
+ +
+ + + +
+ + ); +} + +function ErrorTimeseriesChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const dateFormatter = timeFormatter('HH:mm:ss'); + + return ( + + + + + + `${roundFloat(d * 100)}%`} + /> + + + + {selectedSignificantTerm !== null ? ( + + ) : null} + + + ); +} + +function roundFloat(n: number, digits = 2) { + const factor = Math.pow(10, digits); + return Math.round(n * factor) / factor; +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx new file mode 100644 index 0000000000000..4364731501b89 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ScaleType, + Chart, + LineSeries, + Axis, + CurveType, + BarSeries, + Position, + timeFormatter, + Settings, +} from '@elastic/charts'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { + APIReturnType, + callApmApi, +} from '../../../services/rest/createCallApmApi'; +import { SignificantTermsTable } from './SignificantTermsTable'; +import { ChartContainer } from '../../shared/charts/chart_container'; + +type CorrelationsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/slow_transactions'> +>; + +type SignificantTerm = NonNullable< + CorrelationsApiResponse['significantTerms'] +>[0]; + +export function LatencyCorrelations() { + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { transactionName, transactionType, start, end } = urlParams; + + const { data, status } = useFetcher(() => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/slow_transactions', + params: { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + uiFilters: JSON.stringify(uiFilters), + durationPercentile: '50', + fieldNames: + 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name', + }, + }, + }); + } + }, [serviceName, start, end, transactionName, transactionType, uiFilters]); + + return ( + <> + + + + + +

Average latency over time

+
+ +
+ + +

Latency distribution

+
+ +
+
+
+ + + +
+ + ); +} + +function getTimeseriesYMax(data?: CorrelationsApiResponse) { + if (!data?.overall) { + return 0; + } + + const yValues = [ + ...data.overall.timeseries.map((p) => p.y ?? 0), + ...data.significantTerms.flatMap((term) => + term.timeseries.map((p) => p.y ?? 0) + ), + ]; + return Math.max(...yValues); +} + +function getDistributionYMax(data?: CorrelationsApiResponse) { + if (!data?.overall) { + return 0; + } + + const yValues = [ + ...data.overall.distribution.map((p) => p.y ?? 0), + ...data.significantTerms.flatMap((term) => + term.distribution.map((p) => p.y ?? 0) + ), + ]; + return Math.max(...yValues); +} + +function LatencyTimeseriesChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const dateFormatter = timeFormatter('HH:mm:ss'); + + const yMax = getTimeseriesYMax(data); + const durationFormatter = getDurationFormatter(yMax); + + return ( + + + + + + durationFormatter(d).formatted} + /> + + + + {selectedSignificantTerm !== null ? ( + + ) : null} + + + ); +} + +function LatencyDistributionChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const xMax = Math.max( + ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) + ); + const durationFormatter = getDurationFormatter(xMax); + const yMax = getDistributionYMax(data); + + return ( + + + { + const start = durationFormatter(obj.value); + const end = durationFormatter( + obj.value + data?.distributionInterval + ); + + return `${start.value} - ${end.formatted}`; + }, + }} + /> + durationFormatter(d).formatted} + /> + `${d}%`} + domain={{ min: 0, max: yMax }} + /> + + `${roundFloat(d)}%`} + /> + + {selectedSignificantTerm !== null ? ( + `${roundFloat(d)}%`} + /> + ) : null} + + + ); +} + +function roundFloat(n: number, digits = 2) { + const factor = Math.pow(10, digits); + return Math.round(n * factor) / factor; +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx new file mode 100644 index 0000000000000..b74517902f89b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBadge, EuiIcon, EuiToolTip, EuiLink } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { EuiBasicTable } from '@elastic/eui'; +import { asPercent, asInteger } from '../../../../common/utils/formatters'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { createHref } from '../../shared/Links/url_helpers'; + +type CorrelationsApiResponse = + | APIReturnType<'GET /api/apm/correlations/failed_transactions'> + | APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + +type SignificantTerm = NonNullable< + NonNullable['significantTerms'] +>[0]; + +interface Props { + significantTerms?: T[]; + status: FETCH_STATUS; + setSelectedSignificantTerm: (term: T | null) => void; +} + +export function SignificantTermsTable({ + significantTerms, + status, + setSelectedSignificantTerm, +}: Props) { + const history = useHistory(); + const columns = [ + { + field: 'matches', + name: 'Matches', + render: (_: any, term: T) => { + return ( + + <> + 0.03 ? 'primary' : 'secondary' + } + > + {asPercent(term.fgCount, term.bgCount)} + + ({Math.round(term.score)}) + + + ); + }, + }, + { + field: 'fieldName', + name: 'Field name', + }, + { + field: 'filedValue', + name: 'Field value', + render: (_: any, term: T) => String(term.fieldValue).slice(0, 50), + }, + { + field: 'filedValue', + name: '', + render: (_: any, term: T) => { + return ( + <> + + + + + + + + ); + }, + }, + ]; + + return ( + { + return { + onMouseEnter: () => setSelectedSignificantTerm(term), + onMouseLeave: () => setSelectedSignificantTerm(null), + }; + }} + /> + ); +} 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 e3dea70a232eb..b0f6b83485e39 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx @@ -4,82 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import url from 'url'; -import { useParams } from 'react-router-dom'; -import { useLocation } from 'react-router-dom'; -import { EuiTitle, EuiListGroup } from '@elastic/eui'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiPortal, + EuiCode, + EuiLink, + EuiCallOut, + EuiButton, +} from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { enableCorrelations } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; - -const SESSION_STORAGE_KEY = 'apm.debug.show_correlations'; +import { LatencyCorrelations } from './LatencyCorrelations'; +import { ErrorCorrelations } from './ErrorCorrelations'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { createHref } from '../../shared/Links/url_helpers'; export function Correlations() { - const location = useLocation(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { urlParams, uiFilters } = useUrlParams(); - const { core } = useApmPluginContext(); - const { transactionName, transactionType, start, end } = urlParams; - - if ( - !location.search.includes('&_show_correlations') && - sessionStorage.getItem(SESSION_STORAGE_KEY) !== 'true' - ) { + const { uiSettings } = useApmPluginContext().core; + const { urlParams } = useUrlParams(); + const history = useHistory(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + if (!uiSettings.get(enableCorrelations)) { return null; } - sessionStorage.setItem(SESSION_STORAGE_KEY, 'true'); - - const query = { - serviceName, - transactionName, - transactionType, - start, - end, - uiFilters: JSON.stringify(uiFilters), - fieldNames: - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name', - }; - - const listItems = [ - { - label: 'Show correlations between two ranges', - href: url.format({ - query: { - ...query, - gap: 24, - }, - pathname: core.http.basePath.prepend(`/api/apm/correlations/ranges`), - }), - isDisabled: false, - iconType: 'tokenRange', - size: 's' as const, - }, - - { - label: 'Show correlations for slow transactions', - href: url.format({ - query: { - ...query, - durationPercentile: 95, - }, - pathname: core.http.basePath.prepend( - `/api/apm/correlations/slow_durations` - ), - }), - isDisabled: false, - iconType: 'clock', - size: 's' as const, - }, - ]; - return ( <> - -

Correlations

-
+ { + setIsFlyoutVisible(true); + }} + > + View correlations + + + {isFlyoutVisible && ( + + setIsFlyoutVisible(false)} + > + + +

Correlations

+
+
+ + {urlParams.kuery ? ( + + Filtering by + {urlParams.kuery} + + Clear + + + ) : null} - + + + +
+
+ )} ); } diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 5202ca13ed102..777ee014d3e58 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -20,8 +20,7 @@ import { first } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { px, unit, units } from '../../../../style/variables'; @@ -56,7 +55,9 @@ const TransactionLinkName = styled.div` `; interface Props { - errorGroup: ErrorGroupAPIResponse; + errorGroup: APIReturnType< + 'GET /api/apm/services/{serviceName}/errors/{groupId}' + >; urlParams: IUrlParams; location: Location; } diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index a17bf7e93e466..fd656b8be6ec7 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -18,11 +18,14 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React from 'react'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { ErrorDistributionAPIResponse } from '../../../../../server/lib/errors/distribution/get_distribution'; import { useTheme } from '../../../../hooks/useTheme'; +type ErrorDistributionAPIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/errors/distribution' +>; + interface FormattedBucket { x0: number; x: number; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index e1f6239112555..bfa426985d1c6 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -10,9 +10,8 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import { EuiIconTip } from '@elastic/eui'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupListAPIResponse } from '../../../../../server/lib/errors/get_error_groups'; import { fontFamilyCode, fontSizes, @@ -49,6 +48,10 @@ const Culprit = styled.div` font-family: ${fontFamilyCode}; `; +type ErrorGroupListAPIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/errors' +>; + interface Props { items: ErrorGroupListAPIResponse; serviceName: string; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index f96dc14e34264..63fb69d6d7cbf 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -13,8 +13,8 @@ import { APMRouteDefinition } from '../../../../application/routes'; import { toQuery } from '../../../shared/Links/url_helpers'; import { ErrorGroupDetails } from '../../ErrorGroupDetails'; import { Home } from '../../Home'; -import { ServiceDetails } from '../../ServiceDetails'; -import { ServiceNodeMetrics } from '../../ServiceNodeMetrics'; +import { ServiceDetails } from '../../service_details'; +import { ServiceNodeMetrics } from '../../service_node_metrics'; import { Settings } from '../../Settings'; import { AgentConfigurations } from '../../Settings/AgentConfigurations'; import { AnomalyDetection } from '../../Settings/anomaly_detection'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts index 4610205cee7ed..7ce9d3f25354c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts @@ -8,6 +8,7 @@ import { FetchDataParams, HasDataParams, UxFetchDataResponse, + UXHasDataResponse, } from '../../../../../observability/public/'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -35,7 +36,9 @@ export const fetchUxOverviewDate = async ({ }; }; -export async function hasRumData({ absoluteTime }: HasDataParams) { +export async function hasRumData({ + absoluteTime, +}: HasDataParams): Promise { return await callApmApi({ endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index 1628a664a6c27..8463da0824bde 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -65,21 +65,19 @@ export function ServiceStatsList({ title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { defaultMessage: 'Trans. error rate (avg.)', }), - description: isNumber(avgErrorRate) ? asPercent(avgErrorRate, 1) : null, + description: asPercent(avgErrorRate, 1, ''), }, { title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { defaultMessage: 'CPU usage (avg.)', }), - description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : null, + description: asPercent(avgCpuUsage, 1, ''), }, { title: i18n.translate('xpack.apm.serviceMap.avgMemoryUsagePopoverStat', { defaultMessage: 'Memory usage (avg.)', }), - description: isNumber(avgMemoryUsage) - ? asPercent(avgMemoryUsage, 1) - : null, + description: asPercent(avgMemoryUsage, 1, ''), }, ]; diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 5c9677e3c7af2..89c5c801a5683 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -128,7 +128,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { }), field: 'cpu', sortable: true, - render: (value: number | null) => asPercent(value || 0, 1), + render: (value: number | null) => asPercent(value, 1), }, { name: i18n.translate('xpack.apm.jvmsTable.heapMemoryColumnLabel', { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 3483ad0822801..adae50db85ada 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -8,13 +8,14 @@ import React, { useState } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; -import { callApmApi } from '../../../../../services/rest/createCallApmApi'; +import { + APIReturnType, + callApmApi, +} from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; -type Config = AgentConfigurationListAPIResponse[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; interface Props { config: Config; 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 a67df86b21b1e..81079d78a148a 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 @@ -16,9 +16,8 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; import { useTheme } from '../../../../../hooks/useTheme'; @@ -32,7 +31,7 @@ import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -type Config = AgentConfigurationListAPIResponse[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; interface Props { status: FETCH_STATUS; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/Documentation.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/Documentation.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FiltersSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FiltersSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FlyoutFooter.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FlyoutFooter.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts similarity index 99% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts index 5f8e0b9052a65..4af9321152da3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts @@ -6,7 +6,7 @@ import { getSelectOptions, replaceTemplateVariables, -} from '../CustomLinkFlyout/helper'; +} from '../CreateEditCustomLinkFlyout/helper'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; describe('Custom link helper', () => { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index 9687846d6c520..c6566af3a8b61 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -37,7 +37,7 @@ interface Props { const filtersEmptyState: Filter[] = [{ key: '', value: '' }]; -export function CustomLinkFlyout({ +export function CreateEditCustomLinkFlyout({ onClose, onSave, onDelete, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx index 3a2aa01ba3bc4..7fa8e3a025956 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { LinkPreview } from '../CustomLinkFlyout/LinkPreview'; +import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview'; import { render, getNodeText, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/saveCustomLink.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/saveCustomLink.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx deleted file mode 100644 index 2017aa42e1c5a..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -export function Title() { - return ( - - - - - -

- {i18n.translate('xpack.apm.settings.customizeUI.customLink', { - defaultMessage: 'Custom Links', - })} -

-
-
-
-
-
- ); -} 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 a7feafad11111..96a634828f669 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 @@ -21,7 +21,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; -import * as saveCustomLink from './CustomLinkFlyout/saveCustomLink'; +import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; const data = [ diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index d872f6d21ed96..771a8c6154dc0 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -9,6 +9,7 @@ import { EuiFlexItem, EuiPanel, EuiSpacer, + EuiTitle, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -20,10 +21,9 @@ import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; import { useLicense } from '../../../../../hooks/useLicense'; import { LicensePrompt } from '../../../../shared/LicensePrompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -import { CustomLinkFlyout } from './CustomLinkFlyout'; +import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; import { EmptyPrompt } from './EmptyPrompt'; -import { Title } from './Title'; export function CustomLinkOverview() { const license = useLicense(); @@ -35,9 +35,14 @@ export function CustomLinkOverview() { >(); const { data: customLinks = [], status, refetch } = useFetcher( - (callApmApi) => - callApmApi({ endpoint: 'GET /api/apm/settings/custom_links' }), - [] + async (callApmApi) => { + if (hasValidLicense) { + return callApmApi({ + endpoint: 'GET /api/apm/settings/custom_links', + }); + } + }, + [hasValidLicense] ); useEffect(() => { @@ -61,7 +66,7 @@ export function CustomLinkOverview() { return ( <> {isFlyoutOpen && ( - - + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle> + <EuiFlexGroup + alignItems="center" + gutterSize="s" + responsive={false} + > + <EuiFlexItem grow={false}> + <h2> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink', + { + defaultMessage: 'Custom Links', + } + )} + </h2> + </EuiFlexItem> + </EuiFlexGroup> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> {hasValidLicense && !showEmptyPrompt && ( <EuiFlexItem> diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index 1c21824656754..4704230d7c68c 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -8,8 +8,6 @@ import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroup } from '../../../../server/lib/transaction_groups/fetcher'; import { asMillisecondDuration } from '../../../../common/utils/formatters'; import { fontSizes, truncate } from '../../../style/variables'; import { EmptyMessage } from '../../shared/EmptyMessage'; @@ -17,6 +15,9 @@ import { ImpactBar } from '../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; const StyledTransactionLink = styled(TransactionDetailLink)` font-size: ${fontSizes.large}; @@ -24,11 +25,11 @@ const StyledTransactionLink = styled(TransactionDetailLink)` `; interface Props { - items: TransactionGroup[]; + items: TraceGroup[]; isLoading: boolean; } -const traceListColumns: Array<ITableColumn<TransactionGroup>> = [ +const traceListColumns: Array<ITableColumn<TraceGroup>> = [ { field: 'name', name: i18n.translate('xpack.apm.tracesTable.nameColumnLabel', { @@ -38,7 +39,7 @@ const traceListColumns: Array<ITableColumn<TransactionGroup>> = [ sortable: true, render: ( _: string, - { serviceName, transactionName, transactionType }: TransactionGroup + { serviceName, transactionName, transactionType }: TraceGroup ) => ( <EuiToolTip content={transactionName}> <StyledTransactionLink diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index bf1bda793179f..ac4af7b126468 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -24,18 +24,25 @@ import d3 from 'd3'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { ValuesType } from 'utility-types'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { useTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; +type TransactionDistributionAPIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; + +type DistributionApiResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; + +type DistributionBucket = DistributionApiResponse['buckets'][0]; + interface IChartPoint { x0: number; x: number; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index 3bb23fd6396ca..86221a6e92853 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -17,8 +17,7 @@ import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -28,6 +27,12 @@ import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; +type DistributionApiResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; + +type DistributionBucket = DistributionApiResponse['buckets'][0]; + interface Props { urlParams: IUrlParams; location: Location; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 9d9261fec6c1e..8a99773a97baf 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -21,11 +21,11 @@ import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; import { useWaterfall } from '../../../hooks/useWaterfall'; import { ApmHeader } from '../../shared/ApmHeader'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; +import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { TransactionDistribution } from './Distribution'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context'; +import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; @@ -119,9 +119,9 @@ export function TransactionDetails({ </ApmHeader> <SearchBar /> <EuiPage> - <Correlations /> <EuiFlexGroup> <EuiFlexItem grow={1}> + <Correlations /> <LocalUIFilters {...localUIFiltersConfig} /> </EuiFlexItem> <EuiFlexItem grow={7}> diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx index 049c5934813a2..65dfdd19fa0c5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx @@ -6,11 +6,14 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import { TransactionList } from './'; +type TransactionGroup = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups' +>['items'][0]; + export default { title: 'app/TransactionOverview/TransactionList', component: TransactionList, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx index 7f1dd100d721c..b084d05ee16e8 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx @@ -8,8 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asDecimal, asMillisecondDuration, @@ -21,6 +20,10 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +type TransactionGroup = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups' +>['items'][0]; + // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. const TransactionNameLink = styled(TransactionDetailLink)` diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index a4f8d37867dd5..ff4863e9b8420 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -29,7 +29,7 @@ import { useServiceTransactionTypes } from '../../../hooks/useServiceTransaction import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; +import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; @@ -123,10 +123,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { return ( <> <SearchBar /> - <Correlations /> + <EuiPage> <EuiFlexGroup> <EuiFlexItem grow={1}> + <Correlations /> <LocalUIFilters {...localFiltersConfig}> <TransactionTypeFilter transactionTypes={serviceTransactionTypes} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/service_details/index.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/service_details/index.tsx index 8df2b0fda7a7e..70acc2038e1a7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/index.tsx @@ -8,7 +8,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { ApmHeader } from '../../shared/ApmHeader'; -import { ServiceDetailTabs } from './ServiceDetailTabs'; +import { ServiceDetailTabs } from './service_detail_tabs'; interface Props extends RouteComponentProps<{ serviceName: string }> { tab: React.ComponentProps<typeof ServiceDetailTabs>['tab']; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx rename to x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index f42b94b8afe33..22c5a2b101ddc 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -20,7 +20,7 @@ import { useTransactionOverviewHref } from '../../shared/Links/apm/TransactionOv import { MainTabs } from '../../shared/main_tabs'; import { ErrorGroupOverview } from '../ErrorGroupOverview'; import { ServiceMap } from '../ServiceMap'; -import { ServiceMetrics } from '../ServiceMetrics'; +import { ServiceMetrics } from '../service_metrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../TransactionOverview'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx index 716fed7775f7b..77257f5af7c7e 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; export function ServiceListMetric({ @@ -16,14 +15,8 @@ export function ServiceListMetric({ series?: Array<{ x: number; y: number | null }>; valueLabel: React.ReactNode; }) { - const { - urlParams: { start, end }, - } = useUrlParams(); - return ( <SparkPlotWithValueLabel - start={parseFloat(start!)} - end={parseFloat(end!)} valueLabel={valueLabel} series={series} color={color} 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 3d1572689c5bf..547a0938bc24d 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 @@ -10,14 +10,13 @@ import React from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { asPercent, asDecimal, asMillisecondDuration, } from '../../../../../common/utils/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, px, truncate, unit } from '../../../../style/variables'; import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; @@ -27,12 +26,14 @@ import { AgentIcon } from '../../../shared/AgentIcon'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; +type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; +type Items = ServiceListAPIResponse['items']; + interface Props { - items: ServiceListAPIResponse['items']; + items: Items; noItemsMessage?: React.ReactNode; } - -type ServiceListItem = ValuesType<Props['items']>; +type ServiceListItem = ValuesType<Items>; function formatNumber(value: number) { if (value === 0) { @@ -176,8 +177,7 @@ export const SERVICE_COLUMNS: Array<ITableColumn<ServiceListItem>> = [ render: (_, { transactionErrorRate }) => { const value = transactionErrorRate?.value; - const valueLabel = - value !== null && value !== undefined ? asPercent(value, 1) : ''; + const valueLabel = asPercent(value, 1); return ( <ServiceListMetric 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 73777c2221a5b..39cb73d2a0dd9 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx @@ -7,13 +7,14 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ServiceList, SERVICE_COLUMNS } from './'; import props from './__fixtures__/props.json'; +type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; + function Wrapper({ children }: { children?: ReactNode }) { return ( <MockApmPluginContextWrapper> 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 83f5f4deb89a3..3fa047d840dda 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 @@ -129,9 +129,9 @@ export function ServiceInventory() { <> <SearchBar /> <EuiPage> - <Correlations /> <EuiFlexGroup> <EuiFlexItem grow={1}> + <Correlations /> <LocalUIFilters {...localFiltersConfig} /> </EuiFlexItem> <EuiFlexItem grow={7}> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx similarity index 81% rename from x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/service_metrics/index.tsx index 5808c54d578c6..ded2698c5455d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx @@ -14,9 +14,9 @@ import { } from '@elastic/eui'; import React, { useMemo } from 'react'; import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; -import { MetricsChart } from '../../shared/charts/MetricsChart'; +import { MetricsChart } from '../../shared/charts/metrics_chart'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context'; +import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; @@ -31,7 +31,7 @@ export function ServiceMetrics({ serviceName, }: ServiceMetricsProps) { const { urlParams } = useUrlParams(); - const { data } = useServiceMetricCharts(urlParams, agentName); + const { data, status } = useServiceMetricCharts(urlParams, agentName); const { start, end } = urlParams; const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( @@ -60,7 +60,12 @@ export function ServiceMetrics({ {data.charts.map((chart) => ( <EuiFlexItem key={chart.key}> <EuiPanel> - <MetricsChart start={start} end={end} chart={chart} /> + <MetricsChart + start={start} + end={end} + chart={chart} + fetchStatus={status} + /> </EuiPanel> </EuiFlexItem> ))} diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx rename to x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index efa6110fea100..dd703d445cc60 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -22,14 +22,14 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context'; +import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { useAgentName } from '../../../hooks/useAgentName'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { px, truncate, unit } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; -import { MetricsChart } from '../../shared/charts/MetricsChart'; +import { MetricsChart } from '../../shared/charts/metrics_chart'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; const INITIAL_DATA = { @@ -178,7 +178,12 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { {data.charts.map((chart) => ( <EuiFlexItem key={chart.key}> <EuiPanel> - <MetricsChart start={start} end={end} chart={chart} /> + <MetricsChart + start={start} + end={end} + chart={chart} + fetchStatus={status} + /> </EuiPanel> </EuiFlexItem> ))} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 50667d3135f1a..f734abe27573c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -18,9 +18,9 @@ import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; -import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; import { SearchBar } from '../../shared/search_bar'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; import { TableLinkFlexItem } from './table_link_flex_item'; /** @@ -78,30 +78,7 @@ export function ServiceOverview({ </EuiFlexItem> <EuiFlexItem grow={6}> <EuiPanel> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem> - <EuiTitle size="xs"> - <h2> - {i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableTitle', - { - defaultMessage: 'Transactions', - } - )} - </h2> - </EuiTitle> - </EuiFlexItem> - <TableLinkFlexItem> - <TransactionOverviewLink serviceName={serviceName}> - {i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableLinkText', - { - defaultMessage: 'View transactions', - } - )} - </TransactionOverviewLink> - </TableLinkFlexItem> - </EuiFlexGroup> + <ServiceOverviewTransactionsTable serviceName={serviceName} /> </EuiPanel> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 82dbd6dd86aab..b4228878dd9f5 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -21,10 +21,10 @@ import { px, truncate, unit } from '../../../../style/variables'; import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { ServiceOverviewTable } from '../service_overview_table'; import { TableLinkFlexItem } from '../table_link_flex_item'; -import { FetchWrapper } from './fetch_wrapper'; interface Props { serviceName: string; @@ -135,8 +135,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { }, } )} - start={parseFloat(start!)} - end={parseFloat(end!)} /> ); }, @@ -225,7 +223,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { </EuiFlexGroup> </EuiFlexItem> <EuiFlexItem> - <FetchWrapper status={status}> + <TableFetchWrapper status={status}> <ServiceOverviewTable columns={columns} items={items} @@ -261,7 +259,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { }, }} /> - </FetchWrapper> + </TableFetchWrapper> </EuiFlexItem> </EuiFlexGroup> ); 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 new file mode 100644 index 0000000000000..e91ab338c4a27 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { EuiToolTip } from '@elastic/eui'; +import { ValuesType } from 'utility-types'; +import { + asDuration, + asPercent, + asTransactionRate, +} from '../../../../../common/utils/formatters'; +import { px, truncate, unit } from '../../../../style/variables'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { + APIReturnType, + callApmApi, +} from '../../../../services/rest/createCallApmApi'; +import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; +import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { ImpactBar } from '../../../shared/ImpactBar'; +import { ServiceOverviewTable } from '../service_overview_table'; + +type ServiceTransactionGroupItem = ValuesType< + APIReturnType< + 'GET /api/apm/services/{serviceName}/overview_transaction_groups' + >['transactionGroups'] +>; + +interface Props { + serviceName: string; +} + +type SortField = 'latency' | 'throughput' | 'errorRate' | 'impact'; +type SortDirection = 'asc' | 'desc'; + +const PAGE_SIZE = 5; +const DEFAULT_SORT = { + direction: 'desc' as const, + field: 'impact' as const, +}; + +const TransactionGroupLinkWrapper = styled.div` + width: 100%; + .euiToolTipAnchor { + width: 100% !important; + } +`; + +const StyledTransactionDetailLink = styled(TransactionDetailLink)` + display: block; + ${truncate('100%')} +`; + +export function ServiceOverviewTransactionsTable(props: Props) { + const { serviceName } = props; + + const { + uiFilters, + urlParams: { start, end }, + } = useUrlParams(); + + const [tableOptions, setTableOptions] = useState<{ + pageIndex: number; + sort: { + direction: SortDirection; + field: SortField; + }; + }>({ + pageIndex: 0, + sort: DEFAULT_SORT, + }); + + const { + data = { + totalItemCount: 0, + items: [], + tableOptions: { + pageIndex: 0, + sort: DEFAULT_SORT, + }, + }, + status, + } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/overview_transaction_groups', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + size: PAGE_SIZE, + numBuckets: 20, + pageIndex: tableOptions.pageIndex, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, + }, + }, + }).then((response) => { + return { + items: response.transactionGroups, + totalItemCount: response.totalTransactionGroups, + tableOptions: { + pageIndex: tableOptions.pageIndex, + sort: { + field: tableOptions.sort.field, + direction: tableOptions.sort.direction, + }, + }, + }; + }); + }, [ + serviceName, + start, + end, + uiFilters, + tableOptions.pageIndex, + tableOptions.sort.field, + tableOptions.sort.direction, + ]); + + const { + items, + totalItemCount, + tableOptions: { pageIndex, sort }, + } = data; + + const columns: Array<EuiBasicTableColumn<ServiceTransactionGroupItem>> = [ + { + field: 'name', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnName', + { + defaultMessage: 'Name', + } + ), + render: (_, { name, transactionType }) => { + return ( + <TransactionGroupLinkWrapper> + <EuiToolTip delay="long" content={name}> + <StyledTransactionDetailLink + serviceName={serviceName} + transactionName={name} + transactionType={transactionType} + > + {name} + </StyledTransactionDetailLink> + </EuiToolTip> + </TransactionGroupLinkWrapper> + ); + }, + }, + { + field: 'latency', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency', + { + defaultMessage: 'Latency', + } + ), + width: px(unit * 10), + render: (_, { latency }) => { + return ( + <SparkPlotWithValueLabel + color="euiColorVis1" + compact + series={latency.timeseries ?? undefined} + valueLabel={asDuration(latency.value)} + /> + ); + }, + }, + { + field: 'throughput', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnTroughput', + { + defaultMessage: 'Traffic', + } + ), + width: px(unit * 10), + render: (_, { throughput }) => { + return ( + <SparkPlotWithValueLabel + color="euiColorVis0" + compact + series={throughput.timeseries ?? undefined} + valueLabel={asTransactionRate(throughput.value)} + /> + ); + }, + }, + { + field: 'error_rate', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnErrorRate', + { + defaultMessage: 'Error rate', + } + ), + width: px(unit * 8), + render: (_, { errorRate }) => { + return ( + <SparkPlotWithValueLabel + color="euiColorVis7" + compact + series={errorRate.timeseries ?? undefined} + valueLabel={asPercent(errorRate.value, 1)} + /> + ); + }, + }, + { + field: 'impact', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnImpact', + { + defaultMessage: 'Impact', + } + ), + width: px(unit * 5), + render: (_, { impact }) => { + return <ImpactBar value={impact ?? 0} size="m" />; + }, + }, + ]; + + return ( + <EuiFlexGroup direction="column"> + <EuiFlexItem> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiTitle size="xs"> + <h2> + {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableTitle', + { + defaultMessage: 'Transactions', + } + )} + </h2> + </EuiTitle> + </EuiFlexItem> + <TableLinkFlexItem> + <TransactionOverviewLink serviceName={serviceName}> + {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableLinkText', + { + defaultMessage: 'View transactions', + } + )} + </TransactionOverviewLink> + </TableLinkFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexItem> + <TableFetchWrapper status={status}> + <ServiceOverviewTable + columns={columns} + items={items} + pagination={{ + pageIndex, + pageSize: PAGE_SIZE, + totalItemCount, + pageSizeOptions: [PAGE_SIZE], + hidePerPageOptions: true, + }} + loading={status === FETCH_STATUS.LOADING} + onChange={(newTableOptions: { + page?: { + index: number; + }; + sort?: { field: string; direction: SortDirection }; + }) => { + setTableOptions({ + pageIndex: newTableOptions.page?.index ?? 0, + sort: newTableOptions.sort + ? { + field: newTableOptions.sort.field as SortField, + direction: newTableOptions.sort.direction, + } + : DEFAULT_SORT, + }); + }} + sorting={{ + enableAllColumns: true, + sort: { + direction: sort.direction, + field: sort.field, + }, + }} + /> + </TableFetchWrapper> + </EuiFlexItem> + </EuiFlexItem> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx index ed931191cfb96..f5d71ad15f1ce 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx @@ -10,11 +10,23 @@ import React from 'react'; // TODO: extend from EUI's EuiProgress prop interface export interface ImpactBarProps extends Record<string, unknown> { value: number; + size?: 'l' | 'm'; max?: number; } -export function ImpactBar({ value, max = 100, ...rest }: ImpactBarProps) { +export function ImpactBar({ + value, + size = 'l', + max = 100, + ...rest +}: ImpactBarProps) { return ( - <EuiProgress size="l" value={value} max={max} color="primary" {...rest} /> + <EuiProgress + size={size} + value={value} + max={max} + color="primary" + {...rest} + /> ); } diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 991735a450724..9da26b3fcefac 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { History } from 'history'; import { parse, stringify } from 'query-string'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; import { LocalUIFilterName } from '../../../../common/ui_filter'; @@ -20,6 +21,48 @@ export function fromQuery(query: Record<string, any>) { return stringify(encodedQuery, { sort: false, encode: false }); } +type LocationWithQuery = Partial< + History['location'] & { + query: Record<string, string>; + } +>; + +function getNextLocation( + history: History, + locationWithQuery: LocationWithQuery +) { + const { query, ...rest } = locationWithQuery; + return { + ...history.location, + ...rest, + search: fromQuery({ + ...toQuery(history.location.search), + ...query, + }), + }; +} + +export function replace( + history: History, + locationWithQuery: LocationWithQuery +) { + const location = getNextLocation(history, locationWithQuery); + return history.replace(location); +} + +export function push(history: History, locationWithQuery: LocationWithQuery) { + const location = getNextLocation(history, locationWithQuery); + return history.push(location); +} + +export function createHref( + history: History, + locationWithQuery: LocationWithQuery +) { + const location = getNextLocation(history, locationWithQuery); + return history.createHref(location); +} + export type APMQueryParams = { transactionId?: string; transactionName?: string; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx deleted file mode 100644 index 62952d1fb501b..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { act, fireEvent, render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { CustomLinkPopover } from './CustomLinkPopover'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - <MemoryRouter> - <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper> - </MemoryRouter> - ); -} - -describe('CustomLinkPopover', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'http://elastic.co' }, - { - id: '2', - label: 'bar', - url: 'http://elastic.co?service.name={{service.name}}', - }, - ] as CustomLink[]; - const transaction = ({ - service: { name: 'foo.bar' }, - } as unknown) as Transaction; - it('renders popover', () => { - const component = render( - <CustomLinkPopover - customLinks={customLinks} - transaction={transaction} - onCreateCustomLinkClick={jest.fn()} - onClose={jest.fn()} - />, - { wrapper: Wrapper } - ); - expectTextsInDocument(component, ['CUSTOM LINKS', 'Create', 'foo', 'bar']); - }); - - it('closes popover', () => { - const handleCloseMock = jest.fn(); - const { getByText } = render( - <CustomLinkPopover - customLinks={customLinks} - transaction={transaction} - onCreateCustomLinkClick={jest.fn()} - onClose={handleCloseMock} - />, - { wrapper: Wrapper } - ); - expect(handleCloseMock).not.toHaveBeenCalled(); - act(() => { - fireEvent.click(getByText('CUSTOM LINKS')); - }); - expect(handleCloseMock).toHaveBeenCalled(); - }); - - it('opens flyout to create new custom link', () => { - const handleCreateCustomLinkClickMock = jest.fn(); - const { getByText } = render( - <CustomLinkPopover - customLinks={customLinks} - transaction={transaction} - onCreateCustomLinkClick={handleCreateCustomLinkClickMock} - onClose={jest.fn()} - />, - { wrapper: Wrapper } - ); - expect(handleCreateCustomLinkClickMock).not.toHaveBeenCalled(); - act(() => { - fireEvent.click(getByText('Create')); - }); - expect(handleCreateCustomLinkClickMock).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx deleted file mode 100644 index 27c6aa82ac674..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.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 React from 'react'; -import { - EuiPopoverTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { CustomLinkSection } from './CustomLinkSection'; -import { ManageCustomLink } from './ManageCustomLink'; -import { px } from '../../../../style/variables'; - -const ScrollableContainer = styled.div` - -ms-overflow-style: none; - max-height: ${px(535)}; - overflow: scroll; -`; - -export function CustomLinkPopover({ - customLinks, - onCreateCustomLinkClick, - onClose, - transaction, -}: { - customLinks: CustomLink[]; - onCreateCustomLinkClick: () => void; - onClose: () => void; - transaction: Transaction; -}) { - return ( - <> - <EuiPopoverTitle> - <EuiFlexGroup> - <EuiFlexItem style={{ alignItems: 'flex-start' }}> - <EuiButtonEmpty - color="text" - size="xs" - onClick={onClose} - iconType="arrowLeft" - style={{ fontWeight: 'bold' }} - flush="left" - > - {i18n.translate( - 'xpack.apm.transactionActionMenu.customLink.popover.title', - { - defaultMessage: 'CUSTOM LINKS', - } - )} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem> - <ManageCustomLink - onCreateCustomLinkClick={onCreateCustomLinkClick} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPopoverTitle> - <ScrollableContainer> - <CustomLinkSection - customLinks={customLinks} - transaction={transaction} - /> - </ScrollableContainer> - </> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx deleted file mode 100644 index 6b421bc370332..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { EuiLink, EuiText } from '@elastic/eui'; -import Mustache from 'mustache'; -import React from 'react'; -import styled from 'styled-components'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { px, truncate, units } from '../../../../style/variables'; - -const LinkContainer = styled.li` - margin-top: ${px(units.half)}; - &:first-of-type { - margin-top: 0; - } -`; - -const TruncateText = styled(EuiText)` - font-weight: 500; - line-height: ${px(units.unit)}; - ${truncate(px(units.unit * 25))} -`; - -export function CustomLinkSection({ - customLinks, - transaction, -}: { - customLinks: CustomLink[]; - transaction: Transaction; -}) { - return ( - <ul> - {customLinks.map((link) => { - let href = link.url; - try { - href = Mustache.render(link.url, transaction); - } catch (e) { - // ignores any error that happens - } - return ( - <LinkContainer key={link.id}> - <EuiLink href={href} target="_blank"> - <TruncateText size="s">{link.label}</TruncateText> - </EuiLink> - </LinkContainer> - ); - })} - </ul> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx deleted file mode 100644 index d6484f52e84f9..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx +++ /dev/null @@ -1,128 +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 { - EuiText, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiButtonEmpty, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; -import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { - ActionMenuDivider, - SectionSubtitle, -} from '../../../../../../observability/public'; -import { CustomLinkSection } from './CustomLinkSection'; -import { ManageCustomLink } from './ManageCustomLink'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { LoadingStatePrompt } from '../../LoadingStatePrompt'; -import { px } from '../../../../style/variables'; - -const SeeMoreButton = styled.button<{ show: boolean }>` - display: ${(props) => (props.show ? 'flex' : 'none')}; - align-items: center; - width: 100%; - justify-content: space-between; - &:hover { - text-decoration: underline; - } -`; - -export function CustomLink({ - customLinks, - status, - onCreateCustomLinkClick, - onSeeMoreClick, - transaction, -}: { - customLinks: CustomLinkType[]; - status: FETCH_STATUS; - onCreateCustomLinkClick: () => void; - onSeeMoreClick: () => void; - transaction: Transaction; -}) { - const renderEmptyPrompt = ( - <> - <EuiText size="xs" grow={false} style={{ width: px(300) }}> - {i18n.translate('xpack.apm.customLink.empty', { - defaultMessage: - 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.', - })} - </EuiText> - <EuiSpacer size="s" /> - <EuiButtonEmpty - iconType="plusInCircle" - size="xs" - onClick={onCreateCustomLinkClick} - > - {i18n.translate('xpack.apm.customLink.buttom.create', { - defaultMessage: 'Create custom link', - })} - </EuiButtonEmpty> - </> - ); - - const renderCustomLinkBottomSection = isEmpty(customLinks) ? ( - renderEmptyPrompt - ) : ( - <SeeMoreButton onClick={onSeeMoreClick} show={customLinks.length > 3}> - <EuiText size="s"> - {i18n.translate('xpack.apm.transactionActionMenu.customLink.seeMore', { - defaultMessage: 'See more', - })} - </EuiText> - <EuiIcon type="arrowRight" /> - </SeeMoreButton> - ); - - return ( - <> - <ActionMenuDivider /> - <EuiFlexGroup> - <EuiFlexItem style={{ justifyContent: 'center' }}> - <EuiText size={'s'} grow={false}> - <h5> - {i18n.translate( - 'xpack.apm.transactionActionMenu.customLink.section', - { - defaultMessage: 'Custom Links', - } - )} - </h5> - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <ManageCustomLink - onCreateCustomLinkClick={onCreateCustomLinkClick} - showCreateCustomLinkButton={!!customLinks.length} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="s" /> - <SectionSubtitle> - {i18n.translate('xpack.apm.transactionActionMenu.customLink.subtitle', { - defaultMessage: 'Links will open in a new window.', - })} - </SectionSubtitle> - <CustomLinkSection - customLinks={customLinks.slice(0, 3)} - transaction={transaction} - /> - <EuiSpacer size="s" /> - {status === FETCH_STATUS.LOADING ? ( - <LoadingStatePrompt /> - ) : ( - renderCustomLinkBottomSection - )} - </> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx similarity index 82% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx index 88a4137b47200..16d526bda2103 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import { CustomLinkSection } from './CustomLinkSection'; +import { CustomLinkList } from './CustomLinkList'; import { expectTextsInDocument, expectTextsNotInDocument, @@ -13,7 +13,7 @@ import { import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -describe('CustomLinkSection', () => { +describe('CustomLinkList', () => { const customLinks = [ { id: '1', label: 'foo', url: 'http://elastic.co' }, { @@ -27,14 +27,14 @@ describe('CustomLinkSection', () => { } as unknown) as Transaction; it('shows links', () => { const component = render( - <CustomLinkSection customLinks={customLinks} transaction={transaction} /> + <CustomLinkList customLinks={customLinks} transaction={transaction} /> ); expectTextsInDocument(component, ['foo', 'bar']); }); it('doesnt show any links', () => { const component = render( - <CustomLinkSection customLinks={[]} transaction={transaction} /> + <CustomLinkList customLinks={[]} transaction={transaction} /> ); expectTextsNotInDocument(component, ['foo', 'bar']); }); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx new file mode 100644 index 0000000000000..0304b850d6cee --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Mustache from 'mustache'; +import React from 'react'; +import { + SectionLinks, + SectionLink, +} from '../../../../../../observability/public'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { px, unit } from '../../../../style/variables'; + +export function CustomLinkList({ + customLinks, + transaction, +}: { + customLinks: CustomLink[]; + transaction: Transaction; +}) { + return ( + <SectionLinks style={{ maxHeight: px(unit * 10), overflowY: 'auto' }}> + {customLinks.map((link) => { + const href = getHref(link, transaction); + return ( + <SectionLink + key={link.id} + label={link.label} + href={href} + target="_blank" + /> + ); + })} + </SectionLinks> + ); +} + +function getHref(link: CustomLink, transaction: Transaction) { + try { + return Mustache.render(link.url, transaction); + } catch (e) { + return link.url; + } +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx index 29e93a47629b3..0241167aba1fb 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx @@ -12,7 +12,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../utils/testHelpers'; -import { ManageCustomLink } from './ManageCustomLink'; +import { CustomLinkToolbar } from './CustomLinkToolbar'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -22,23 +22,20 @@ function Wrapper({ children }: { children?: ReactNode }) { ); } -describe('ManageCustomLink', () => { +describe('CustomLinkToolbar', () => { it('renders with create button', () => { - const component = render( - <ManageCustomLink onCreateCustomLinkClick={jest.fn()} />, - { wrapper: Wrapper } - ); + const component = render(<CustomLinkToolbar onClickCreate={jest.fn()} />, { + wrapper: Wrapper, + }); expect( component.getByLabelText('Custom links settings page') ).toBeInTheDocument(); expectTextsInDocument(component, ['Create']); }); + it('renders without create button', () => { const component = render( - <ManageCustomLink - onCreateCustomLinkClick={jest.fn()} - showCreateCustomLinkButton={false} - />, + <CustomLinkToolbar onClickCreate={jest.fn()} showCreateButton={false} />, { wrapper: Wrapper } ); expect( @@ -46,12 +43,11 @@ describe('ManageCustomLink', () => { ).toBeInTheDocument(); expectTextsNotInDocument(component, ['Create']); }); + it('opens flyout to create new custom link', () => { const handleCreateCustomLinkClickMock = jest.fn(); const { getByText } = render( - <ManageCustomLink - onCreateCustomLinkClick={handleCreateCustomLinkClickMock} - />, + <CustomLinkToolbar onClickCreate={handleCreateCustomLinkClickMock} />, { wrapper: Wrapper } ); expect(handleCreateCustomLinkClickMock).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx similarity index 85% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx index 09cdaa26004bb..36b370b4069ae 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx @@ -14,12 +14,12 @@ import { import { i18n } from '@kbn/i18n'; import { APMLink } from '../../Links/apm/APMLink'; -export function ManageCustomLink({ - onCreateCustomLinkClick, - showCreateCustomLinkButton = true, +export function CustomLinkToolbar({ + onClickCreate, + showCreateButton = true, }: { - onCreateCustomLinkClick: () => void; - showCreateCustomLinkButton?: boolean; + onClickCreate: () => void; + showCreateButton?: boolean; }) { return ( <EuiFlexGroup> @@ -41,12 +41,12 @@ export function ManageCustomLink({ </APMLink> </EuiToolTip> </EuiFlexItem> - {showCreateCustomLinkButton && ( + {showCreateButton && ( <EuiFlexItem grow={false}> <EuiButtonEmpty iconType="plusInCircle" size="xs" - onClick={onCreateCustomLinkClick} + onClick={onClickCreate} > {i18n.translate('xpack.apm.customLink.buttom.create.title', { defaultMessage: 'Create', diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx index 5abeae265dfa6..db7a284f6adff 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx @@ -7,11 +7,11 @@ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { CustomLink } from '.'; +import { CustomLinkMenuSection } from '.'; import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import * as useFetcher from '../../../../hooks/useFetcher'; import { expectTextsInDocument, expectTextsNotInDocument, @@ -25,16 +25,27 @@ function Wrapper({ children }: { children?: ReactNode }) { ); } +const transaction = ({ + service: { + name: 'name', + environment: 'env', + }, + transaction: { + name: 'tx name', + type: 'tx type', + }, +} as unknown) as Transaction; + describe('Custom links', () => { it('shows empty message when no custom link is available', () => { + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: [], + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); @@ -45,14 +56,14 @@ describe('Custom links', () => { }); it('shows loading while custom links are fetched', () => { + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: [], + status: useFetcher.FETCH_STATUS.LOADING, + refetch: jest.fn(), + }); + const { getByTestId } = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.LOADING} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expect(getByTestId('loading-spinner')).toBeInTheDocument(); @@ -65,61 +76,68 @@ describe('Custom links', () => { { id: '3', label: 'baz', url: 'baz' }, { id: '4', label: 'qux', url: 'qux' }, ] as CustomLinkType[]; + + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: customLinks, + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expectTextsInDocument(component, ['foo', 'bar', 'baz']); expectTextsNotInDocument(component, ['qux']); }); - it('clicks on See more button', () => { + it('clicks "show all" and "show fewer"', () => { const customLinks = [ { id: '1', label: 'foo', url: 'foo' }, { id: '2', label: 'bar', url: 'bar' }, { id: '3', label: 'baz', url: 'baz' }, { id: '4', label: 'qux', url: 'qux' }, ] as CustomLinkType[]; - const onSeeMoreClickMock = jest.fn(); + + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: customLinks, + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={onSeeMoreClickMock} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); - expect(onSeeMoreClickMock).not.toHaveBeenCalled(); + + expect(component.getAllByRole('listitem').length).toEqual(3); + act(() => { + fireEvent.click(component.getByText('Show all')); + }); + expect(component.getAllByRole('listitem').length).toEqual(4); act(() => { - fireEvent.click(component.getByText('See more')); + fireEvent.click(component.getByText('Show fewer')); }); - expect(onSeeMoreClickMock).toHaveBeenCalled(); + expect(component.getAllByRole('listitem').length).toEqual(3); }); describe('create custom link buttons', () => { it('shows create button below empty message', () => { + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: [], + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expectTextsInDocument(component, ['Create custom link']); expectTextsNotInDocument(component, ['Create']); }); + it('shows create button besides the title', () => { const customLinks = [ { id: '1', label: 'foo', url: 'foo' }, @@ -127,14 +145,15 @@ describe('Custom links', () => { { id: '3', label: 'baz', url: 'baz' }, { id: '4', label: 'qux', url: 'qux' }, ] as CustomLinkType[]; + + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: customLinks, + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expectTextsInDocument(component, ['Create']); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx new file mode 100644 index 0000000000000..2825363b10197 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useMemo, useState } from 'react'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { + ActionMenuDivider, + Section, + SectionSubtitle, + SectionTitle, +} from '../../../../../../observability/public'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { CustomLinkList } from './CustomLinkList'; +import { CustomLinkToolbar } from './CustomLinkToolbar'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { LoadingStatePrompt } from '../../LoadingStatePrompt'; +import { px } from '../../../../style/variables'; +import { CreateEditCustomLinkFlyout } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout'; +import { convertFiltersToQuery } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper'; +import { + CustomLink, + Filter, +} from '../../../../../common/custom_link/custom_link_types'; + +const DEFAULT_LINKS_TO_SHOW = 3; + +export function CustomLinkMenuSection({ + transaction, +}: { + transaction: Transaction; +}) { + const [showAllLinks, setShowAllLinks] = useState(false); + const [isCreateEditFlyoutOpen, setIsCreateEditFlyoutOpen] = useState(false); + + const filters = useMemo( + () => + [ + { key: 'service.name', value: transaction?.service.name }, + { key: 'service.environment', value: transaction?.service.environment }, + { key: 'transaction.name', value: transaction?.transaction.name }, + { key: 'transaction.type', value: transaction?.transaction.type }, + ].filter((filter): filter is Filter => typeof filter.value === 'string'), + [transaction] + ); + + const { data: customLinks = [], status, refetch } = useFetcher( + (callApmApi) => + callApmApi({ + isCachable: true, + endpoint: 'GET /api/apm/settings/custom_links', + params: { query: convertFiltersToQuery(filters) }, + }), + [filters] + ); + + return ( + <> + {isCreateEditFlyoutOpen && ( + <CreateEditCustomLinkFlyout + defaults={{ filters }} + onClose={() => { + setIsCreateEditFlyoutOpen(false); + }} + onSave={() => { + setIsCreateEditFlyoutOpen(false); + refetch(); + }} + onDelete={() => { + setIsCreateEditFlyoutOpen(false); + refetch(); + }} + /> + )} + + <ActionMenuDivider /> + + <Section> + <EuiFlexGroup> + <EuiFlexItem> + <SectionTitle> + {i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.section', + { + defaultMessage: 'Custom Links', + } + )} + </SectionTitle> + </EuiFlexItem> + <EuiFlexItem> + <CustomLinkToolbar + onClickCreate={() => setIsCreateEditFlyoutOpen(true)} + showCreateButton={customLinks.length > 0} + /> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + <SectionSubtitle> + {i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.subtitle', + { + defaultMessage: 'Links will open in a new window.', + } + )} + </SectionSubtitle> + <CustomLinkList + customLinks={ + showAllLinks + ? customLinks + : customLinks.slice(0, DEFAULT_LINKS_TO_SHOW) + } + transaction={transaction} + /> + <EuiSpacer size="s" /> + <BottomSection + status={status} + customLinks={customLinks} + showAllLinks={showAllLinks} + toggleShowAll={() => setShowAllLinks((show) => !show)} + onClickCreate={() => setIsCreateEditFlyoutOpen(true)} + /> + </Section> + </> + ); +} + +function BottomSection({ + status, + customLinks, + showAllLinks, + toggleShowAll, + onClickCreate, +}: { + status: FETCH_STATUS; + customLinks: CustomLink[]; + showAllLinks: boolean; + toggleShowAll: () => void; + onClickCreate: () => void; +}) { + if (status === FETCH_STATUS.LOADING) { + return <LoadingStatePrompt />; + } + + // render empty prompt if there are no custom links + if (isEmpty(customLinks)) { + return ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiText size="xs" grow={false} style={{ width: px(300) }}> + {i18n.translate('xpack.apm.customLink.empty', { + defaultMessage: + 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.', + })} + </EuiText> + <EuiSpacer size="s" /> + <EuiButtonEmpty + iconType="plusInCircle" + size="xs" + onClick={onClickCreate} + > + {i18n.translate('xpack.apm.customLink.buttom.create', { + defaultMessage: 'Create custom link', + })} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + + // render button to toggle "Show all" / "Show fewer" + if (customLinks.length > DEFAULT_LINKS_TO_SHOW) { + return ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiButtonEmpty + iconType={showAllLinks ? 'arrowUp' : 'arrowDown'} + onClick={toggleShowAll} + > + <EuiText size="s"> + {showAllLinks + ? i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.showFewer', + { defaultMessage: 'Show fewer' } + ) + : i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.showAll', + { defaultMessage: 'Show all' } + )} + </EuiText> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + + return null; +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index f5a57544209f5..15a85113406e1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -6,7 +6,7 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { ActionMenu, @@ -17,16 +17,11 @@ import { SectionSubtitle, SectionTitle, } from '../../../../../observability/public'; -import { Filter } from '../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { CustomLinkFlyout } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout'; -import { convertFiltersToQuery } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper'; -import { CustomLink } from './CustomLink'; -import { CustomLinkPopover } from './CustomLink/CustomLinkPopover'; +import { CustomLinkMenuSection } from './CustomLinkMenuSection'; import { getSections } from './sections'; interface Props { @@ -45,37 +40,13 @@ function ActionMenuButton({ onClick }: { onClick: () => void }) { export function TransactionActionMenu({ transaction }: Props) { const license = useLicense(); - const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); + const hasGoldLicense = license?.isActive && license?.hasAtLeast('gold'); const { core } = useApmPluginContext(); const location = useLocation(); const { urlParams } = useUrlParams(); const [isActionPopoverOpen, setIsActionPopoverOpen] = useState(false); - const [isCustomLinksPopoverOpen, setIsCustomLinksPopoverOpen] = useState( - false - ); - const [isCustomLinkFlyoutOpen, setIsCustomLinkFlyoutOpen] = useState(false); - - const filters = useMemo( - () => - [ - { key: 'service.name', value: transaction?.service.name }, - { key: 'service.environment', value: transaction?.service.environment }, - { key: 'transaction.name', value: transaction?.transaction.name }, - { key: 'transaction.type', value: transaction?.transaction.type }, - ].filter((filter): filter is Filter => typeof filter.value === 'string'), - [transaction] - ); - - const { data: customLinks = [], status, refetch } = useFetcher( - (callApmApi) => - callApmApi({ - endpoint: 'GET /api/apm/settings/custom_links', - params: { query: convertFiltersToQuery(filters) }, - }), - [filters] - ); const sections = getSections({ transaction, @@ -84,39 +55,11 @@ export function TransactionActionMenu({ transaction }: Props) { urlParams, }); - const closePopover = () => { - setIsActionPopoverOpen(false); - setIsCustomLinksPopoverOpen(false); - }; - - const toggleCustomLinkFlyout = () => { - closePopover(); - setIsCustomLinkFlyoutOpen((isOpen) => !isOpen); - }; - - const toggleCustomLinkPopover = () => { - setIsCustomLinksPopoverOpen((isOpen) => !isOpen); - }; - return ( <> - {isCustomLinkFlyoutOpen && ( - <CustomLinkFlyout - defaults={{ filters }} - onClose={toggleCustomLinkFlyout} - onSave={() => { - toggleCustomLinkFlyout(); - refetch(); - }} - onDelete={() => { - toggleCustomLinkFlyout(); - refetch(); - }} - /> - )} <ActionMenu id="transactionActionMenu" - closePopover={closePopover} + closePopover={() => setIsActionPopoverOpen(false)} isOpen={isActionPopoverOpen} anchorPosition="downRight" button={ @@ -124,52 +67,34 @@ export function TransactionActionMenu({ transaction }: Props) { } > <div> - {isCustomLinksPopoverOpen ? ( - <CustomLinkPopover - customLinks={customLinks.slice(3, customLinks.length)} - onCreateCustomLinkClick={toggleCustomLinkFlyout} - onClose={toggleCustomLinkPopover} - transaction={transaction} - /> - ) : ( - <> - {sections.map((section, idx) => { - const isLastSection = idx !== sections.length - 1; - return ( - <div key={idx}> - {section.map((item) => ( - <Section key={item.key}> - {item.title && ( - <SectionTitle>{item.title}</SectionTitle> - )} - {item.subtitle && ( - <SectionSubtitle>{item.subtitle}</SectionSubtitle> - )} - <SectionLinks> - {item.actions.map((action) => ( - <SectionLink - key={action.key} - label={action.label} - href={action.href} - /> - ))} - </SectionLinks> - </Section> - ))} - {isLastSection && <ActionMenuDivider />} - </div> - ); - })} - {hasValidLicense && ( - <CustomLink - customLinks={customLinks} - status={status} - onCreateCustomLinkClick={toggleCustomLinkFlyout} - onSeeMoreClick={toggleCustomLinkPopover} - transaction={transaction} - /> - )} - </> + {sections.map((section, idx) => { + const isLastSection = idx !== sections.length - 1; + return ( + <div key={idx}> + {section.map((item) => ( + <Section key={item.key}> + {item.title && <SectionTitle>{item.title}</SectionTitle>} + {item.subtitle && ( + <SectionSubtitle>{item.subtitle}</SectionSubtitle> + )} + <SectionLinks> + {item.actions.map((action) => ( + <SectionLink + key={action.key} + label={action.label} + href={action.href} + /> + ))} + </SectionLinks> + </Section> + ))} + {isLastSection && <ActionMenuDivider />} + </div> + ); + })} + + {hasGoldLicense && ( + <CustomLinkMenuSection transaction={transaction} /> )} </div> </ActionMenu> diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 05cae589c19fc..677e4b7593ff1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -8,6 +8,7 @@ import { AreaSeries, Axis, Chart, + CurveType, niceTimeFormatter, Placement, Position, @@ -103,6 +104,7 @@ export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) { stackAccessors={['x']} stackMode={'percentage'} color={serie.areaColor} + curve={CurveType.CURVE_MONOTONE_X} /> ); }) diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx deleted file mode 100644 index 9fc16ab0f9eab..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx +++ /dev/null @@ -1,73 +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 { VerticalGridLines } from 'react-vis'; -import { - EuiIcon, - EuiToolTip, - EuiFlexGroup, - EuiFlexItem, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { asAbsoluteDateTime } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/useTheme'; -import { Maybe } from '../../../../../typings/common'; -import { Annotation } from '../../../../../common/annotations'; -import { PlotValues, SharedPlot } from './plotUtils'; - -interface Props { - annotations: Annotation[]; - plotValues: PlotValues; - width: number; - overlay: Maybe<HTMLElement>; -} - -export function AnnotationsPlot({ plotValues, annotations }: Props) { - const theme = useTheme(); - const tickValues = annotations.map((annotation) => annotation['@timestamp']); - - const style = { - stroke: theme.eui.euiColorSecondary, - strokeDasharray: 'none', - }; - - return ( - <> - <SharedPlot plotValues={plotValues}> - <VerticalGridLines tickValues={tickValues} style={style} /> - </SharedPlot> - {annotations.map((annotation) => ( - <div - key={annotation.id} - style={{ - position: 'absolute', - left: plotValues.x(annotation['@timestamp']) - 8, - top: -2, - }} - > - <EuiToolTip - title={asAbsoluteDateTime(annotation['@timestamp'], 'seconds')} - content={ - <EuiFlexGroup> - <EuiFlexItem grow={true}> - <EuiText> - {i18n.translate('xpack.apm.version', { - defaultMessage: 'Version', - })} - </EuiText> - </EuiFlexItem> - <EuiFlexItem grow={false}>{annotation.text}</EuiFlexItem> - </EuiFlexGroup> - } - > - <EuiIcon type="dot" color={theme.eui.euiColorSecondary} /> - </EuiToolTip> - </div> - ))} - </> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx deleted file mode 100644 index e70c53108cb0e..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx +++ /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 { storiesOf } from '@storybook/react'; -import React from 'react'; -// @ts-expect-error -import CustomPlot from './'; - -storiesOf('shared/charts/CustomPlot', module).add( - 'with annotations but no data', - () => { - const annotations = [ - { - type: 'version', - id: '2020-06-10 04:36:31', - '@timestamp': 1591763925012, - text: '2020-06-10 04:36:31', - }, - { - type: 'version', - id: '2020-06-10 15:23:01', - '@timestamp': 1591802689233, - text: '2020-06-10 15:23:01', - }, - ]; - return <CustomPlot annotations={annotations} series={[]} />; - }, - { - info: { - source: false, - text: - "When a chart has no data but does have annotations, the annotations shouldn't show up at all.", - }, - } -); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js deleted file mode 100644 index 5aa315d599e18..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash'; -import { SharedPlot } from './plotUtils'; -import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; -import SelectionMarker from './SelectionMarker'; - -import { MarkSeries, VerticalGridLines } from 'react-vis'; -import Tooltip from '../Tooltip'; - -function getPointByX(serie, x) { - return serie.data.find((point) => point.x === x); -} - -class InteractivePlot extends PureComponent { - getMarkPoints = (hoverX) => { - return ( - this.props.series - .filter((serie) => - serie.data.some((point) => point.x === hoverX && point.y != null) - ) - .map((serie) => { - const { x, y } = getPointByX(serie, hoverX) || {}; - return { - x, - y, - color: serie.color, - }; - }) - // needs to be reversed, as StaticPlot.js does the same - .reverse() - ); - }; - - getTooltipPoints = (hoverX) => { - return this.props.series - .filter((series) => !series.hideTooltipValue) - .map((serie) => { - const point = getPointByX(serie, hoverX) || {}; - return { - color: serie.color, - value: this.props.formatTooltipValue(point), - text: serie.titleShort || serie.title, - }; - }); - }; - - render() { - const { - plotValues, - hoverX, - series, - isDrawing, - selectionStart, - selectionEnd, - } = this.props; - - if (isEmpty(series)) { - return null; - } - - const tooltipPoints = this.getTooltipPoints(hoverX); - const markPoints = this.getMarkPoints(hoverX); - const { x, xTickValues, yTickValues } = plotValues; - const yValueMiddle = yTickValues[1]; - - if (isEmpty(xTickValues)) { - return <SharedPlot plotValues={plotValues} />; - } - - return ( - <SharedPlot plotValues={plotValues}> - {hoverX && ( - <Tooltip tooltipPoints={tooltipPoints} x={hoverX} y={yValueMiddle} /> - )} - - {hoverX && <MarkSeries data={markPoints} colorType="literal" />} - {hoverX && <VerticalGridLines tickValues={[hoverX]} />} - - {isDrawing && selectionEnd !== null && ( - <SelectionMarker start={x(selectionStart)} end={x(selectionEnd)} /> - )} - </SharedPlot> - ); - } -} - -InteractivePlot.propTypes = { - formatTooltipValue: PropTypes.func.isRequired, - hoverX: PropTypes.number, - isDrawing: PropTypes.bool.isRequired, - plotValues: PropTypes.object.isRequired, - selectionEnd: PropTypes.number, - selectionStart: PropTypes.number, - series: PropTypes.array.isRequired, -}; - -export default InteractivePlot; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js deleted file mode 100644 index 2c4cc185dac7e..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { Legend } from '../Legend'; -import { useTheme } from '../../../../hooks/useTheme'; -import { - unit, - units, - fontSizes, - px, - truncate, -} from '../../../../style/variables'; -import { i18n } from '@kbn/i18n'; -import { EuiIcon } from '@elastic/eui'; - -const Container = styled.div` - display: flex; - margin-left: ${px(unit * 5)}; - flex-wrap: wrap; - - /* add margin to all direct descendant divs */ - & > div { - margin-top: ${px(units.half)}; - margin-right: ${px(unit)}; - &:last-child { - margin-right: 0; - } - } -`; - -const LegendContent = styled.span` - white-space: nowrap; - color: ${({ theme }) => theme.eui.euiColorMediumShade}; - display: flex; -`; - -const TruncatedLabel = styled.span` - display: inline-block; - ${truncate(px(units.half * 10))}; -`; - -const SeriesValue = styled.span` - margin-left: ${px(units.quarter)}; - color: ${({ theme }) => theme.eui.euiColorFullShade}; - display: inline-block; -`; - -const MoreSeriesContainer = styled.div` - font-size: ${fontSizes.small}; - color: ${({ theme }) => theme.eui.euiColorMediumShade}; -`; - -function MoreSeries({ hiddenSeriesCount }) { - if (hiddenSeriesCount <= 0) { - return null; - } - - return ( - <MoreSeriesContainer> - (+ - {hiddenSeriesCount}) - </MoreSeriesContainer> - ); -} - -export default function Legends({ - clickLegend, - hiddenSeriesCount, - noHits, - series, - seriesEnabledState, - truncateLegends, - hasAnnotations, - showAnnotations, - onAnnotationsToggle, -}) { - const theme = useTheme(); - - if (noHits && !hasAnnotations) { - return null; - } - - return ( - <Container> - {series.map((serie, i) => { - if (serie.hideLegend) { - return null; - } - - const text = ( - <LegendContent> - {truncateLegends ? ( - <TruncatedLabel title={serie.title}>{serie.title}</TruncatedLabel> - ) : ( - serie.title - )} - {serie.legendValue && ( - <SeriesValue>{serie.legendValue}</SeriesValue> - )} - </LegendContent> - ); - return ( - <Legend - key={i} - onClick={ - serie.legendClickDisabled ? undefined : () => clickLegend(i) - } - disabled={seriesEnabledState[i]} - text={text} - color={serie.color} - /> - ); - })} - {hasAnnotations && ( - <Legend - key="annotations" - onClick={() => { - if (onAnnotationsToggle) { - onAnnotationsToggle(); - } - }} - text={ - <LegendContent> - {i18n.translate('xpack.apm.serviceVersion', { - defaultMessage: 'Service version', - })} - </LegendContent> - } - indicator={() => ( - <div style={{ marginRight: px(units.quarter) }}> - <EuiIcon type="annotation" color={theme.eui.euiColorSecondary} /> - </div> - )} - disabled={!showAnnotations} - color={theme.eui.euiColorSecondary} - /> - )} - <MoreSeries hiddenSeriesCount={hiddenSeriesCount} /> - </Container> - ); -} - -Legends.propTypes = { - clickLegend: PropTypes.func.isRequired, - hiddenSeriesCount: PropTypes.number.isRequired, - noHits: PropTypes.bool.isRequired, - series: PropTypes.array.isRequired, - seriesEnabledState: PropTypes.array.isRequired, - truncateLegends: PropTypes.bool.isRequired, - hasAnnotations: PropTypes.bool, - showAnnotations: PropTypes.bool, - onAnnotationsToggle: PropTypes.func, -}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js deleted file mode 100644 index a4286578d44d1..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; - -function SelectionMarker({ innerHeight, marginTop, start, end }) { - const width = Math.abs(end - start); - const x = start < end ? start : end; - return ( - <rect - pointerEvents="none" - fill="black" - fillOpacity="0.1" - x={x} - y={marginTop} - width={width} - height={innerHeight} - /> - ); -} - -SelectionMarker.requiresSVG = true; -SelectionMarker.propTypes = { - start: PropTypes.number, - end: PropTypes.number, -}; - -export default SelectionMarker; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js deleted file mode 100644 index e49899da85e0d..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ /dev/null @@ -1,243 +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 { - XAxis, - YAxis, - HorizontalGridLines, - LineSeries, - LineMarkSeries, - AreaSeries, - VerticalRectSeries, -} from 'react-vis'; -import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; -import { last } from 'lodash'; -import { rgba } from 'polished'; -import { scaleUtc } from 'd3-scale'; - -import StatusText from './StatusText'; -import { SharedPlot } from './plotUtils'; -import { i18n } from '@kbn/i18n'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs'; - -// undefined values are converted by react-vis into NaN when stacking -// see https://github.com/uber/react-vis/issues/1214 -const getNull = (d) => isValidCoordinateValue(d.y) && !isNaN(d.y); - -class StaticPlot extends PureComponent { - getVisSeries(series, plotValues) { - return series - .slice() - .reverse() - .map((serie) => this.getSerie(serie, plotValues)); - } - - getSerie(serie, plotValues) { - switch (serie.type) { - case 'line': - return ( - <LineSeries - getNull={getNull} - key={serie.title} - xType="time-utc" - curve={'curveMonotoneX'} - data={serie.data} - color={serie.color} - stack={serie.stack} - /> - ); - case 'area': - return ( - <AreaSeries - getNull={getNull} - key={serie.title} - xType="time-utc" - curve={'curveMonotoneX'} - data={serie.data} - color={serie.color} - stroke={serie.color} - fill={serie.areaColor || rgba(serie.color, 0.3)} - /> - ); - - case 'areaStacked': { - // convert null into undefined because of stack issues, - // see https://github.com/uber/react-vis/issues/1214 - const data = serie.data.map((value) => { - return 'y' in value && isValidCoordinateValue(value.y) - ? value - : { ...value, y: undefined }; - }); - - // make sure individual markers are displayed in cases - // where there are gaps - - const markersForGaps = serie.data.map((value, index) => { - const prevHasData = getNull(serie.data[index - 1] ?? {}); - const nextHasData = getNull(serie.data[index + 1] ?? {}); - const thisHasData = getNull(value); - - const isGap = !prevHasData && !nextHasData && thisHasData; - - if (!isGap) { - return { - ...value, - y: undefined, - }; - } - - return value; - }); - - return [ - <AreaSeries - getNull={getNull} - key={`${serie.title}-area`} - xType="time-utc" - curve={'curveMonotoneX'} - data={data} - color={serie.color} - stroke={'rgba(0,0,0,0)'} - fill={serie.areaColor || rgba(serie.color, 0.3)} - stack={true} - cluster="area" - />, - <LineSeries - getNull={getNull} - key={`${serie.title}-line`} - xType="time-utc" - curve={'curveMonotoneX'} - data={data} - color={serie.color} - stack={true} - cluster="line" - />, - <LineMarkSeries - getNull={getNull} - key={`${serie.title}-line-markers`} - xType="time-utc" - curve={'curveMonotoneX'} - data={markersForGaps} - stroke={serie.color} - color={serie.color} - lineStyle={{ - opacity: 0, - }} - stack={true} - cluster="line-mark" - size={1} - />, - ]; - } - - case 'areaMaxHeight': - const yMax = last(plotValues.yTickValues); - const data = serie.data.map((p) => ({ - x0: p.x0, - x: p.x, - y0: 0, - y: yMax, - })); - - return ( - <VerticalRectSeries - getNull={getNull} - key={serie.title} - xType="time-utc" - curve={'curveMonotoneX'} - data={data} - color={serie.color} - stroke={serie.color} - fill={serie.areaColor} - /> - ); - case 'linemark': - return ( - <LineMarkSeries - getNull={getNull} - key={serie.title} - xType="time-utc" - curve={'curveMonotoneX'} - data={serie.data} - color={serie.color} - size={1} - /> - ); - default: - throw new Error(`Unknown type ${serie.type}`); - } - } - - /** - * A tick format function that takes the timezone from Kibana's settings into - * account. Used if no tickFormatX prop is supplied. - * - * This produces the same results as the built-in formatter from D3, which is - * what react-vis uses, but shifts the timezone. - */ - tickFormatXTime = (value) => { - const xDomain = this.props.plotValues.x.domain(); - - const time = value.getTime(); - - return scaleUtc().domain(xDomain).tickFormat()( - new Date(time - getTimezoneOffsetInMs(time)) - ); - }; - - render() { - const { series, tickFormatY, plotValues, noHits } = this.props; - const { xTickValues, yTickValues } = plotValues; - - const tickFormatX = this.props.tickFormatX || this.tickFormatXTime; - - return ( - <SharedPlot plotValues={plotValues}> - <XAxis - type="time-utc" - tickSize={0} - tickFormat={tickFormatX} - tickValues={xTickValues} - /> - {noHits ? ( - <StatusText - marginLeft={30} - text={i18n.translate('xpack.apm.metrics.plot.noDataLabel', { - defaultMessage: 'No data within this time range.', - })} - /> - ) : ( - [ - <HorizontalGridLines key="grid-lines" tickValues={yTickValues} />, - <YAxis - key="y-axis" - tickSize={0} - tickValues={yTickValues} - tickFormat={tickFormatY} - style={{ - line: { stroke: 'none', fill: 'none' }, - }} - />, - this.getVisSeries(series, plotValues), - ] - )} - </SharedPlot> - ); - } -} - -export default StaticPlot; - -StaticPlot.propTypes = { - noHits: PropTypes.bool.isRequired, - series: PropTypes.array.isRequired, - plotValues: PropTypes.object.isRequired, - tickFormatX: PropTypes.func, - tickFormatY: PropTypes.func.isRequired, - width: PropTypes.number.isRequired, -}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js deleted file mode 100644 index 51cb3c3885765..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; - -/** - * NOTE: The margin props in this component are being magically - * set from react-vis by way of the makeFlexibleWidth helper, - * unless specifically set and overridden from above. - */ - -function StatusText({ - marginLeft, - marginRight, - marginTop, - marginBottom, - text, -}) { - const xTransform = `calc(-50% + ${marginLeft - marginRight}px)`; - const yTransform = `calc(-50% + ${marginTop - marginBottom}px - 15px)`; - - return ( - <div - style={{ - position: 'absolute', - top: '50%', - left: '50%', - transform: `translate(${xTransform},${yTransform})`, - }} - > - {text} - </div> - ); -} - -StatusText.propTypes = { - text: PropTypes.string, -}; - -export default StatusText; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js deleted file mode 100644 index 26b03672f1c1f..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { union } from 'lodash'; -import { Voronoi } from 'react-vis'; -import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; - -import { SharedPlot } from './plotUtils'; - -function getXValuesCombined(series) { - return union(...series.map((serie) => serie.data.map((p) => p.x))).map( - (x) => ({ - x, - }) - ); -} - -class VoronoiPlot extends PureComponent { - render() { - const { series, plotValues, noHits } = this.props; - const { XY_MARGIN, XY_HEIGHT, XY_WIDTH, x } = plotValues; - const xValuesCombined = getXValuesCombined(series); - if (!xValuesCombined || noHits) { - return null; - } - - return ( - <SharedPlot - plotValues={plotValues} - onMouseLeave={this.props.onMouseLeave} - > - <Voronoi - extent={[ - [XY_MARGIN.left, XY_MARGIN.top], - [XY_WIDTH, XY_HEIGHT], - ]} - nodes={xValuesCombined} - onHover={this.props.onHover} - onMouseDown={this.props.onMouseDown} - onMouseUp={this.props.onMouseUp} - x={(d) => x(d.x)} - y={() => 0} - /> - </SharedPlot> - ); - } -} - -export default VoronoiPlot; - -VoronoiPlot.propTypes = { - noHits: PropTypes.bool.isRequired, - onHover: PropTypes.func.isRequired, - onMouseDown: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired, - onMouseUp: PropTypes.func, - series: PropTypes.array.isRequired, - plotValues: PropTypes.object.isRequired, -}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js deleted file mode 100644 index 501d30b5e2ba1..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ /dev/null @@ -1,262 +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 { isEmpty, flatten } from 'lodash'; -import { makeWidthFlexible } from 'react-vis'; -import PropTypes from 'prop-types'; -import React, { PureComponent, Fragment } from 'react'; - -import Legends from './Legends'; -import StaticPlot from './StaticPlot'; -import InteractivePlot from './InteractivePlot'; -import VoronoiPlot from './VoronoiPlot'; -import { AnnotationsPlot } from './AnnotationsPlot'; -import { createSelector } from 'reselect'; -import { getPlotValues } from './plotUtils'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; - -const VISIBLE_LEGEND_COUNT = 4; - -function getHiddenLegendCount(series) { - return series.filter((serie) => serie.hideLegend).length; -} - -export class InnerCustomPlot extends PureComponent { - state = { - seriesEnabledState: [], - isDrawing: false, - selectionStart: null, - selectionEnd: null, - showAnnotations: true, - }; - - getEnabledSeries = createSelector( - (state) => state.visibleSeries, - (state) => state.seriesEnabledState, - (visibleSeries, seriesEnabledState) => - visibleSeries.filter((serie, i) => !seriesEnabledState[i]) - ); - - getOptions = createSelector( - (state) => state.width, - (state) => state.yMin, - (state) => state.yMax, - (state) => state.height, - (state) => state.stackBy, - (width, yMin, yMax, height, stackBy) => ({ - width, - yMin, - yMax, - height, - stackBy, - }) - ); - - getPlotValues = createSelector( - (state) => state.visibleSeries, - (state) => state.enabledSeries, - (state) => state.options, - getPlotValues - ); - - getVisibleSeries = createSelector( - (state) => state.series, - (series) => { - return series.slice( - 0, - this.props.visibleLegendCount + getHiddenLegendCount(series) - ); - } - ); - - clickLegend = (i) => { - this.setState(({ seriesEnabledState }) => { - const nextSeriesEnabledState = this.props.series.map((value, _i) => { - const disabledValue = seriesEnabledState[_i]; - return i === _i ? !disabledValue : !!disabledValue; - }); - - if (typeof this.props.onToggleLegend === 'function') { - this.props.onToggleLegend(nextSeriesEnabledState); - } - - return { - seriesEnabledState: nextSeriesEnabledState, - }; - }); - }; - - onMouseLeave = (...args) => { - this.props.onMouseLeave(...args); - }; - - onMouseDown = (node) => - this.setState({ - isDrawing: true, - selectionStart: node.x, - selectionEnd: null, - }); - - onMouseUp = () => { - if (this.state.isDrawing && this.state.selectionEnd !== null) { - const [start, end] = [ - this.state.selectionStart, - this.state.selectionEnd, - ].sort(); - this.props.onSelectionEnd({ start, end }); - } - this.setState({ isDrawing: false }); - }; - - onHover = (node) => { - this.props.onHover(node.x); - - if (this.state.isDrawing) { - this.setState({ selectionEnd: node.x }); - } - }; - - componentDidMount() { - document.body.addEventListener('mouseup', this.onMouseUp); - } - - componentWillUnmount() { - document.body.removeEventListener('mouseup', this.onMouseUp); - } - - render() { - const { - series, - truncateLegends, - width, - annotations, - visibleLegendCount, - } = this.props; - - if (!width) { - return null; - } - - const hiddenSeriesCount = Math.max( - series.length - visibleLegendCount - getHiddenLegendCount(series), - 0 - ); - const visibleSeries = this.getVisibleSeries({ series }); - const enabledSeries = this.getEnabledSeries({ - visibleSeries, - seriesEnabledState: this.state.seriesEnabledState, - }); - const options = this.getOptions(this.props); - - const hasValidCoordinates = flatten(series.map((s) => s.data)).some((p) => - isValidCoordinateValue(p.y) - ); - const noHits = this.props.noHits || !hasValidCoordinates; - - const plotValues = this.getPlotValues({ - visibleSeries, - enabledSeries: enabledSeries, - options, - }); - - if (isEmpty(plotValues)) { - return null; - } - - return ( - <Fragment> - <div style={{ position: 'relative', height: plotValues.XY_HEIGHT }}> - <StaticPlot - width={width} - noHits={noHits} - plotValues={plotValues} - series={enabledSeries} - tickFormatY={this.props.tickFormatY} - tickFormatX={this.props.tickFormatX} - /> - - {this.state.showAnnotations && !isEmpty(annotations) && !noHits && ( - <AnnotationsPlot - plotValues={plotValues} - width={width} - annotations={annotations || []} - /> - )} - - <InteractivePlot - plotValues={plotValues} - hoverX={this.props.hoverX} - series={enabledSeries} - formatTooltipValue={this.props.formatTooltipValue} - isDrawing={this.state.isDrawing} - selectionStart={this.state.selectionStart} - selectionEnd={this.state.selectionEnd} - /> - - <VoronoiPlot - noHits={noHits} - plotValues={plotValues} - series={enabledSeries} - onHover={this.onHover} - onMouseLeave={this.onMouseLeave} - onMouseDown={this.onMouseDown} - /> - </div> - <Legends - noHits={noHits} - truncateLegends={truncateLegends} - series={visibleSeries} - hiddenSeriesCount={hiddenSeriesCount} - clickLegend={this.clickLegend} - seriesEnabledState={this.state.seriesEnabledState} - hasAnnotations={!isEmpty(annotations) && !noHits} - showAnnotations={this.state.showAnnotations} - onAnnotationsToggle={() => { - this.setState(({ showAnnotations }) => ({ - showAnnotations: !showAnnotations, - })); - }} - /> - </Fragment> - ); - } -} - -InnerCustomPlot.propTypes = { - formatTooltipValue: PropTypes.func, - hoverX: PropTypes.number, - onHover: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired, - onSelectionEnd: PropTypes.func.isRequired, - series: PropTypes.array.isRequired, - tickFormatY: PropTypes.func, - truncateLegends: PropTypes.bool, - width: PropTypes.number.isRequired, - height: PropTypes.number, - stackBy: PropTypes.string, - annotations: PropTypes.arrayOf( - PropTypes.shape({ - type: PropTypes.string, - id: PropTypes.string, - firstSeen: PropTypes.number, - }) - ), - noHits: PropTypes.bool, - visibleLegendCount: PropTypes.number, - onToggleLegend: PropTypes.func, -}; - -InnerCustomPlot.defaultProps = { - formatTooltipValue: (p) => p.y, - tickFormatX: undefined, - tickFormatY: (y) => y, - truncateLegends: false, - xAxisTickSizeOuter: 0, - noHits: false, - visibleLegendCount: VISIBLE_LEGEND_COUNT, -}; - -export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts deleted file mode 100644 index 117ec26446de8..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as plotUtils from './plotUtils'; -import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; - -describe('plotUtils', () => { - describe('getPlotValues', () => { - describe('with empty arguments', () => { - it('returns plotvalues', () => { - expect( - plotUtils.getPlotValues([], [], { height: 1, width: 1 }) - ).toMatchObject({ - XY_HEIGHT: 1, - XY_WIDTH: 1, - }); - }); - }); - - describe('when yMin is given', () => { - it('uses the yMin in the scale', () => { - expect( - plotUtils - .getPlotValues([], [], { height: 1, width: 1, yMin: 100 }) - .y.domain()[0] - ).toEqual(100); - }); - - describe('when yMin is "min"', () => { - it('uses minimum y from the series', () => { - expect( - plotUtils - .getPlotValues( - [ - { data: [{ x: 0, y: 200 }] }, - { data: [{ x: 0, y: 300 }] }, - ] as Array<TimeSeries<Coordinate>>, - [], - { - height: 1, - width: 1, - yMin: 'min', - } - ) - .y.domain()[0] - ).toEqual(200); - }); - }); - }); - - describe('when yMax given', () => { - it('uses yMax', () => { - expect( - plotUtils - .getPlotValues([], [], { - height: 1, - width: 1, - yMax: 500, - }) - .y.domain()[1] - ).toEqual(500); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx deleted file mode 100644 index 67b7fd31b05bc..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx +++ /dev/null @@ -1,147 +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 { isEmpty, flatten } from 'lodash'; -import { scaleLinear } from 'd3-scale'; -import { XYPlot } from 'react-vis'; -import d3 from 'd3'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; -import { unit } from '../../../../style/variables'; -import { getDomainTZ, getTimeTicksTZ } from '../helper/timezone'; - -const XY_HEIGHT = unit * 16; -const XY_MARGIN = { - top: unit, - left: unit * 5, - right: unit, - bottom: unit * 2, -}; - -const getXScale = (xMin: number, xMax: number, width: number) => { - return scaleLinear() - .domain([xMin, xMax]) - .range([XY_MARGIN.left, width - XY_MARGIN.right]); -}; - -const getYScale = (yMin: number, yMax: number) => { - return scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice(); -}; - -function getFlattenedCoordinates( - visibleSeries: Array<TimeSeries<Coordinate>>, - enabledSeries: Array<TimeSeries<Coordinate>> -) { - const enabledCoordinates = flatten(enabledSeries.map((serie) => serie.data)); - if (!isEmpty(enabledCoordinates)) { - return enabledCoordinates; - } - - return flatten(visibleSeries.map((serie) => serie.data)); -} - -export type PlotValues = ReturnType<typeof getPlotValues>; - -export function getPlotValues( - visibleSeries: Array<TimeSeries<Coordinate>>, - enabledSeries: Array<TimeSeries<Coordinate>>, - { - width, - yMin = 0, - yMax = 'max', - height, - stackBy, - }: { - width: number; - yMin?: number | 'min'; - yMax?: number | 'max'; - height: number; - stackBy?: 'x' | 'y'; - } -) { - const flattenedCoordinates = getFlattenedCoordinates( - visibleSeries, - enabledSeries - ); - - const xMin = d3.min(flattenedCoordinates, (d) => d.x); - const xMax = d3.max(flattenedCoordinates, (d) => d.x); - - if (yMax === 'max') { - yMax = d3.max(flattenedCoordinates, (d) => d.y ?? 0); - } - if (yMin === 'min') { - yMin = d3.min(flattenedCoordinates, (d) => d.y ?? 0); - } - - const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax); - - const xScale = getXScale(xMin, xMax, width); - const yScale = getYScale(yMin, yMax); - - const yMaxNice = yScale.domain()[1]; - const yTickValues = [0, yMaxNice / 2, yMaxNice]; - - // approximate number of x-axis ticks based on the width of the plot. There should by approx 1 tick per 100px - // d3 will determine the exact number of ticks based on the selected range - const xTickTotal = Math.floor(width / 100); - - const xTickValues = getTimeTicksTZ({ - domain: [xMinZone, xMaxZone], - totalTicks: xTickTotal, - width, - }); - - return { - x: xScale, - y: yScale, - xTickValues, - yTickValues, - XY_MARGIN, - XY_HEIGHT: height || XY_HEIGHT, - XY_WIDTH: width, - stackBy, - }; -} - -export function SharedPlot({ - plotValues, - ...props -}: { - plotValues: PlotValues; - children: React.ReactNode; -}) { - const { XY_HEIGHT: height, XY_MARGIN: margin, XY_WIDTH: width } = plotValues; - - return ( - <div - style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} - > - <XYPlot - dontCheckIfEmpty - height={height} - margin={margin} - xType="time-utc" - width={width} - xDomain={plotValues.x.domain()} - yDomain={plotValues.y.domain()} - stackBy={plotValues.stackBy} - {...props} - /> - </div> - ); -} - -SharedPlot.propTypes = { - plotValues: PropTypes.shape({ - x: PropTypes.func.isRequired, - y: PropTypes.func.isRequired, - XY_WIDTH: PropTypes.number.isRequired, - height: PropTypes.number, - }).isRequired, -}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js deleted file mode 100644 index 9d127c06e0c14..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; -import React from 'react'; -import { - disableConsoleWarning, - toJson, - mountWithTheme, -} from '../../../../../utils/testHelpers'; -import { InnerCustomPlot } from '../index'; -import responseWithData from './responseWithData.json'; -import VoronoiPlot from '../VoronoiPlot'; -import InteractivePlot from '../InteractivePlot'; -import { getResponseTimeSeries } from '../../../../../selectors/chartSelectors'; -import { getEmptySeries } from '../getEmptySeries'; - -function getXValueByIndex(index) { - return responseWithData.responseTimes.avg[index].x; -} - -describe('when response has data', () => { - let consoleMock; - let wrapper; - let onHover; - let onMouseLeave; - let onSelectionEnd; - - beforeAll(() => { - consoleMock = disableConsoleWarning('Warning: componentWillReceiveProps'); - }); - - afterAll(() => { - consoleMock.mockRestore(); - }); - - beforeEach(() => { - const series = getResponseTimeSeries({ apmTimeseries: responseWithData }); - onHover = jest.fn(); - onMouseLeave = jest.fn(); - onSelectionEnd = jest.fn(); - wrapper = mountWithTheme( - <InnerCustomPlot - series={series} - onHover={onHover} - onMouseLeave={onMouseLeave} - onSelectionEnd={onSelectionEnd} - width={800} - tickFormatX={(x) => x.getTime()} // Avoid timezone issues in snapshots - /> - ); - - // Spy on render methods to determine if they re-render - jest.spyOn(VoronoiPlot.prototype, 'render').mockClear(); - jest.spyOn(InteractivePlot.prototype, 'render').mockClear(); - }); - - describe('Initially', () => { - it('should have 3 enabled series', () => { - expect(wrapper.find('LineSeries').length).toBe(3); - }); - - it('should have 3 legends ', () => { - const legends = wrapper.find('Legend'); - expect(legends.length).toBe(3); - expect(legends.map((e) => e.props())).toMatchSnapshot(); - }); - - it('should have 3 XY plots', () => { - expect(wrapper.find('StaticPlot XYPlot').length).toBe(1); - expect(wrapper.find('InteractivePlot XYPlot').length).toBe(1); - expect(wrapper.find('VoronoiPlot XYPlot').length).toBe(1); - }); - - it('should have correct state', () => { - expect(wrapper.state().seriesEnabledState).toEqual([]); - expect(wrapper.state().isDrawing).toBe(false); - expect(wrapper.state().selectionStart).toBe(null); - expect(wrapper.state().selectionEnd).toBe(null); - expect(wrapper.state()).toMatchSnapshot(); - }); - - it('should not display tooltip', () => { - expect(wrapper.find('Tooltip').length).toEqual(0); - }); - - it('should have correct markup', () => { - expect(toJson(wrapper)).toMatchSnapshot(); - }); - }); - - describe('Legends', () => { - it('should have initial values when nothing is clicked', () => { - expect(wrapper.state('seriesEnabledState')).toEqual([]); - expect(wrapper.find('StaticPlot').prop('series').length).toBe(3); - }); - - describe('when legend is clicked once', () => { - beforeEach(() => { - wrapper.find('Legend').at(1).simulate('click'); - }); - - it('should have 2 enabled series', () => { - expect(wrapper.find('LineSeries').length).toBe(2); - }); - - it('should add disabled prop to Legends', () => { - expect( - wrapper.find('Legend').map((node) => node.prop('disabled')) - ).toEqual([false, true, false]); - }); - - it('should toggle series ', () => { - expect(wrapper.state('seriesEnabledState')).toEqual([ - false, - true, - false, - ]); - expect(wrapper.find('StaticPlot').prop('series').length).toBe(2); - }); - - it('should re-render VoronoiPlot', () => { - expect(VoronoiPlot.prototype.render.mock.calls.length).toBe(1); - }); - - it('should re-render InteractivePlot', () => { - expect(InteractivePlot.prototype.render.mock.calls.length).toEqual(1); - }); - }); - - describe('when legend is clicked twice', () => { - beforeEach(() => { - wrapper.find('Legend').at(1).simulate('click').simulate('click'); - }); - - it('should toggle series back to initial state', () => { - expect( - wrapper.find('Legend').map((node) => node.prop('disabled')) - ).toEqual([false, false, false]); - - expect(wrapper.state('seriesEnabledState')).toEqual([ - false, - false, - false, - ]); - - expect(wrapper.find('StaticPlot').prop('series').length).toBe(3); - }); - - it('should re-render VoronoiPlot', () => { - expect(VoronoiPlot.prototype.render.mock.calls.length).toBe(2); - }); - - it('should re-render InteractivePlot', () => { - expect(InteractivePlot.prototype.render.mock.calls.length).toEqual(2); - }); - }); - }); - - describe('when hovering over', () => { - const index = 22; - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(index).simulate('mouseOver'); - }); - - it('should call onHover', () => { - expect(onHover).toHaveBeenCalledWith(getXValueByIndex(index)); - }); - }); - - describe('when setting hoverX', () => { - beforeEach(() => { - // Avoid timezone issues in snapshots - jest.spyOn(moment.prototype, 'format').mockImplementation(function () { - return this.unix(); - }); - - // Simulate hovering over multiple buckets - wrapper.setProps({ hoverX: getXValueByIndex(13) }); - wrapper.setProps({ hoverX: getXValueByIndex(14) }); - wrapper.setProps({ hoverX: getXValueByIndex(15) }); - }); - - it('should display tooltip', () => { - expect(wrapper.find('Tooltip').length).toEqual(1); - expect(wrapper.find('Tooltip').prop('tooltipPoints')).toMatchSnapshot(); - }); - - it('should display vertical line at correct time', () => { - expect( - wrapper.find('InteractivePlot VerticalGridLines').prop('tickValues') - ).toEqual([1502283720000]); - }); - - it('should not re-render VoronoiPlot', () => { - expect(VoronoiPlot.prototype.render.mock.calls.length).toBe(0); - }); - - it('should re-render InteractivePlot', () => { - expect(InteractivePlot.prototype.render.mock.calls.length).toEqual(3); - }); - - it('should match snapshots', () => { - expect(toJson(wrapper)).toMatchSnapshot(); - expect(wrapper.state()).toMatchSnapshot(); - }); - }); - - describe('when dragging without releasing', () => { - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(10).simulate('mouseDown'); - - wrapper.find('.rv-voronoi__cell').at(20).simulate('mouseOver'); - }); - - it('should display SelectionMarker', () => { - expect(toJson(wrapper.find('SelectionMarker'))).toMatchSnapshot(); - }); - - it('should not call onSelectionEnd', () => { - expect(onSelectionEnd).not.toHaveBeenCalled(); - }); - }); - - describe('when dragging from left to right and releasing', () => { - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(10).simulate('mouseDown'); - - wrapper.find('.rv-voronoi__cell').at(20).simulate('mouseOver'); - document.body.dispatchEvent(new Event('mouseup')); - }); - - it('should call onSelectionEnd', () => { - expect(onSelectionEnd).toHaveBeenCalledWith({ - start: 1502283420000, - end: 1502284020000, - }); - }); - }); - - describe('when dragging from right to left and releasing', () => { - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(20).simulate('mouseDown'); - - wrapper.find('.rv-voronoi__cell').at(10).simulate('mouseOver'); - document.body.dispatchEvent(new Event('mouseup')); - }); - - it('should call onSelectionEnd', () => { - expect(onSelectionEnd).toHaveBeenCalledWith({ - start: 1502283420000, - end: 1502284020000, - }); - }); - }); - - it('should call onMouseLeave when leaving the XY plot', () => { - wrapper.find('VoronoiPlot svg.rv-xy-plot__inner').simulate('mouseLeave'); - expect(onMouseLeave).toHaveBeenCalledWith(expect.any(Object)); - }); -}); - -describe('when response has no data', () => { - const onHover = jest.fn(); - const onMouseLeave = jest.fn(); - const onSelectionEnd = jest.fn(); - const annotations = [ - { - type: 'version', - id: '2020-06-10 04:36:31', - '@timestamp': 1591763925012, - text: '2020-06-10 04:36:31', - }, - { - type: 'version', - id: '2020-06-10 15:23:01', - '@timestamp': 1591802689233, - text: '2020-06-10 15:23:01', - }, - ]; - - let wrapper; - beforeEach(() => { - const series = getEmptySeries(1451606400000, 1451610000000); - - wrapper = mountWithTheme( - <InnerCustomPlot - annotations={annotations} - series={series} - onHover={onHover} - onMouseLeave={onMouseLeave} - onSelectionEnd={onSelectionEnd} - width={800} - tickFormatX={(x) => x.getTime()} // Avoid timezone issues in snapshots - /> - ); - }); - - describe('Initially', () => { - it('should have 0 legends ', () => { - expect(wrapper.find('Legend').length).toBe(0); - }); - - it('should have 2 XY plots', () => { - expect(wrapper.find('StaticPlot XYPlot').length).toBe(1); - expect(wrapper.find('InteractivePlot XYPlot').length).toBe(1); - expect(wrapper.find('VoronoiPlot XYPlot').length).toBe(0); - }); - - it('should have correct state', () => { - expect(wrapper.state().seriesEnabledState).toEqual([]); - expect(wrapper.state().isDrawing).toBe(false); - expect(wrapper.state().selectionStart).toBe(null); - expect(wrapper.state().selectionEnd).toBe(null); - expect(wrapper.state()).toMatchSnapshot(); - }); - - it('should not display tooltip', () => { - expect(wrapper.find('Tooltip').length).toEqual(0); - }); - - it('should not show annotations', () => { - expect(wrapper.find('AnnotationsPlot')).toHaveLength(0); - }); - - it('should have correct markup', () => { - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - it('should have a single series', () => { - expect(wrapper.prop('series').length).toBe(1); - }); - - it('The series is empty and every y-value is null', () => { - expect(wrapper.prop('series')[0].data.every((d) => d.y === null)).toEqual( - true - ); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap deleted file mode 100644 index 20636fa144479..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ /dev/null @@ -1,6436 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`when response has data Initially should have 3 legends 1`] = ` -Array [ - Object { - "color": "#6092c0", - "disabled": undefined, - "onClick": [Function], - "text": <styled.span> - Avg. - <styled.span> - 468 ms - </styled.span> - </styled.span>, - }, - Object { - "color": "#d6bf57", - "disabled": undefined, - "onClick": [Function], - "text": <styled.span> - 95th percentile - </styled.span>, - }, - Object { - "color": "#da8b45", - "disabled": undefined, - "onClick": [Function], - "text": <styled.span> - 99th percentile - </styled.span>, - }, -] -`; - -exports[`when response has data Initially should have correct markup 1`] = ` -Array [ - <div - style={ - Object { - "height": 256, - "position": "relative", - } - } - > - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - > - <g - className="rv-xy-plot__axis rv-xy-plot__axis--horizontal " - style={Object {}} - transform="translate(80,224)" - > - <line - className="rv-xy-plot__axis__line" - style={Object {}} - x1={0} - x2={704} - y1={0} - y2={0} - /> - <g - className="rv-xy-plot__axis__ticks" - transform="translate(0, 0)" - > - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(70.4, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283000000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(187.73333333333332, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283300000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(305.06666666666666, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283600000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(422.4, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283900000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(539.7333333333333, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502284200000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(657.0666666666667, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502284500000 - </text> - </g> - </g> - </g> - <g - className="rv-xy-plot__grid-lines" - transform="translate(80,16)" - > - <line - className="rv-xy-plot__grid-lines__line" - x1={0} - x2={704} - y1={208} - y2={208} - /> - <line - className="rv-xy-plot__grid-lines__line" - x1={0} - x2={704} - y1={104} - y2={104} - /> - <line - className="rv-xy-plot__grid-lines__line" - x1={0} - x2={704} - y1={0} - y2={0} - /> - </g> - <g - className="rv-xy-plot__axis rv-xy-plot__axis--vertical " - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0,16)" - > - <line - className="rv-xy-plot__axis__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={80} - x2={80} - y1={0} - y2={208} - /> - <g - className="rv-xy-plot__axis__ticks" - transform="translate(80, 0)" - > - <g - className="rv-xy-plot__axis__tick" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0, 208)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={0} - x2={-0} - y1={0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.32em" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - textAnchor="end" - transform="translate(-8, 0)" - > - 0 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0, 104)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={0} - x2={-0} - y1={0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.32em" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - textAnchor="end" - transform="translate(-8, 0)" - > - 2500000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={0} - x2={-0} - y1={0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.32em" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - textAnchor="end" - transform="translate(-8, 0)" - > - 5000000 - </text> - </g> - </g> - </g> - <g - className="rv-xy-plot__series rv-xy-plot__series--linemark" - > - <path - className="rv-xy-plot__series rv-xy-plot__series--line " - d="M0,208C7.822222222222222,177.67069216000004,15.644444444444442,147.3413843200001,23.466666666666665,145.82854374400006C31.288888888888888,144.31570316800003,39.11111111111111,143.55928288,46.93333333333333,143.55928288C54.75555555555555,143.55928288,62.57777777777778,143.69173256533335,70.4,143.95663193600006C78.22222222222223,144.22153130666678,86.04444444444444,150.087546752,93.86666666666666,150.087546752C101.68888888888888,150.087546752,109.5111111111111,143.61986468266673,117.33333333333333,141.36762432000006C125.15555555555555,139.1153839573334,132.9777777777778,138.10678017066667,140.8,136.574104576C148.62222222222223,135.04142898133333,156.44444444444446,134.60761100800002,164.26666666666668,132.171570752C172.0888888888889,129.735530496,179.9111111111111,122.7740017919999,187.73333333333332,121.95786303999996C195.55555555555554,121.14172428800003,203.37777777777777,120.73365491200006,211.2,120.73365491200006C219.0222222222222,120.73365491200006,226.84444444444443,150.33875334399997,234.66666666666666,150.33875334399997C242.48888888888888,150.33875334399997,250.3111111111111,145.21265574400005,258.1333333333333,145.21265574400005C265.9555555555556,145.21265574400005,273.77777777777777,159.49950515199998,281.6,159.49950515199998C289.4222222222222,159.49950515199998,297.24444444444447,159.25604087466667,305.06666666666666,158.76911232C312.8888888888889,158.28218376533334,320.7111111111111,153.13340861866664,328.53333333333336,148.71727519999996C336.35555555555555,144.30114178133329,344.1777777777778,136.23707191466664,352,132.27231180799998C359.8222222222222,128.30755170133332,367.64444444444445,127.55802467199999,375.46666666666664,124.92871456C383.2888888888889,122.299404448,391.1111111111111,116.49645113600002,398.93333333333334,116.49645113600002C406.75555555555553,116.49645113600002,414.5777777777778,142.8818553066667,422.4,147.94231920000001C430.22222222222223,153.00278309333333,438.0444444444444,155.53301504,445.8666666666667,155.53301504C453.68888888888887,155.53301504,461.5111111111111,147.5484465173333,469.3333333333333,141.915090304C477.1555555555555,136.28173409066667,484.97777777777776,121.73287776000008,492.79999999999995,121.73287776000008C500.6222222222222,121.73287776000008,508.4444444444444,147.90280169599995,516.2666666666667,152.63684758399998C524.0888888888888,157.370893472,531.9111111111112,159.73791641600002,539.7333333333333,159.73791641600002C547.5555555555555,159.73791641600002,555.3777777777779,140.4438672,563.2,140.4438672C571.0222222222222,140.4438672,578.8444444444445,150.146582976,586.6666666666667,150.146582976C594.4888888888889,150.146582976,602.3111111111111,121.98686448,610.1333333333333,121.98686448C617.9555555555555,121.98686448,625.7777777777778,208,633.6,208C641.4222222222222,208,649.2444444444445,133.8337757120002,657.0666666666667,102.0323582720003C664.8888888888889,70.23094083200053,672.7111111111111,17.191495360000836,680.5333333333333,17.191495360000836C688.3555555555555,17.191495360000836,696.1777777777778,112.59574768000043,704,208" - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - style={ - Object { - "opacity": 1, - "stroke": "#da8b45", - "strokeDasharray": undefined, - "strokeWidth": undefined, - } - } - transform="translate(80,16)" - /> - <g - className="rv-xy-plot__series rv-xy-plot__series--mark " - transform="translate(80,16)" - > - <circle - cx={0} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={23.466666666666665} - cy={145.82854374400006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={46.93333333333333} - cy={143.55928288} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={70.4} - cy={143.95663193600006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={93.86666666666666} - cy={150.087546752} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={117.33333333333333} - cy={141.36762432000006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={140.8} - cy={136.574104576} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={164.26666666666668} - cy={132.171570752} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={187.73333333333332} - cy={121.95786303999996} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={211.2} - cy={120.73365491200006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={234.66666666666666} - cy={150.33875334399997} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={258.1333333333333} - cy={145.21265574400005} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={281.6} - cy={159.49950515199998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={305.06666666666666} - cy={158.76911232} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={328.53333333333336} - cy={148.71727519999996} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={132.27231180799998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={375.46666666666664} - cy={124.92871456} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={398.93333333333334} - cy={116.49645113600002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={422.4} - cy={147.94231920000001} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={445.8666666666667} - cy={155.53301504} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={469.3333333333333} - cy={141.915090304} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={492.79999999999995} - cy={121.73287776000008} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={516.2666666666667} - cy={152.63684758399998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={539.7333333333333} - cy={159.73791641600002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={563.2} - cy={140.4438672} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={586.6666666666667} - cy={150.146582976} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={610.1333333333333} - cy={121.98686448} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={633.6} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={657.0666666666667} - cy={102.0323582720003} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={680.5333333333333} - cy={17.191495360000836} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={704} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - </g> - </g> - <g - className="rv-xy-plot__series rv-xy-plot__series--linemark" - > - <path - className="rv-xy-plot__series rv-xy-plot__series--line " - d="M0,208C7.822222222222222,183.10434549333334,15.644444444444442,158.20869098666665,23.466666666666665,157.4191424C31.288888888888888,156.62959381333334,39.11111111111111,156.23481952,46.93333333333333,156.23481952C54.75555555555555,156.23481952,62.57777777777778,160.81596682666668,70.4,161.56425792000002C78.22222222222223,162.31254901333335,86.04444444444444,162.68669456,93.86666666666666,162.68669456C101.68888888888888,162.68669456,109.5111111111111,158.86059904000004,117.33333333333333,158.86059904000004C125.15555555555555,158.86059904000004,132.9777777777778,163.62246992000001,140.8,163.62246992000001C148.62222222222223,163.62246992000001,156.44444444444446,142.73391392000002,164.26666666666668,142.73391392000002C172.0888888888889,142.73391392000002,179.9111111111111,165.8699744,187.73333333333332,165.8699744C195.55555555555554,165.8699744,203.37777777777777,163.6534529066668,211.2,163.52439168000006C219.0222222222222,163.39533045333334,226.84444444444443,163.45986106666672,234.66666666666666,163.33079984C242.48888888888888,163.20173861333328,250.3111111111111,161.4781168,258.1333333333333,161.4781168C265.9555555555556,161.4781168,273.77777777777777,161.87593552,281.6,162.16472064C289.4222222222222,162.45350576,297.24444444444447,162.51342293333335,305.06666666666666,163.21082752C312.8888888888889,163.90823210666667,320.7111111111111,166.81319824,328.53333333333336,166.81319824C336.35555555555555,166.81319824,344.1777777777778,143.21282560000003,352,143.21282560000003C359.8222222222222,143.21282560000003,367.64444444444445,164.71169104,375.46666666666664,164.71169104C383.2888888888889,164.71169104,391.1111111111111,135.88840304000001,398.93333333333334,135.88840304000001C406.75555555555553,135.88840304000001,414.5777777777778,152.6074260533333,422.4,157.5681224C430.22222222222223,162.52881874666667,438.0444444444444,165.65258112,445.8666666666667,165.65258112C453.68888888888887,165.65258112,461.5111111111111,165.61368234666668,469.3333333333333,165.53588480000002C477.1555555555555,165.45808725333336,484.97777777777776,147.713644,492.79999999999995,147.713644C500.6222222222222,147.713644,508.4444444444444,163.65928869333334,516.2666666666667,164.06490256C524.0888888888888,164.47051642666668,531.9111111111112,164.27093592,539.7333333333333,164.67332336C547.5555555555555,165.07571080000002,555.3777777777779,166.4792272,563.2,166.4792272C571.0222222222222,166.4792272,578.8444444444445,152.7591936,586.6666666666667,152.7591936C594.4888888888889,152.7591936,602.3111111111111,156.23893584,610.1333333333333,163.19842032C617.9555555555555,170.15790480000004,625.7777777777778,208,633.6,208C641.4222222222222,208,649.2444444444445,177.47751424000003,657.0666666666667,161.37461184C664.8888888888889,145.27170944000005,672.7111111111111,111.3825856,680.5333333333333,111.3825856C688.3555555555555,111.3825856,696.1777777777778,159.69129279999999,704,208" - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - style={ - Object { - "opacity": 1, - "stroke": "#d6bf57", - "strokeDasharray": undefined, - "strokeWidth": undefined, - } - } - transform="translate(80,16)" - /> - <g - className="rv-xy-plot__series rv-xy-plot__series--mark " - transform="translate(80,16)" - > - <circle - cx={0} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={23.466666666666665} - cy={157.4191424} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={46.93333333333333} - cy={156.23481952} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={70.4} - cy={161.56425792000002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={93.86666666666666} - cy={162.68669456} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={117.33333333333333} - cy={158.86059904000004} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={140.8} - cy={163.62246992000001} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={164.26666666666668} - cy={142.73391392000002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={187.73333333333332} - cy={165.8699744} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={211.2} - cy={163.52439168000006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={234.66666666666666} - cy={163.33079984} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={258.1333333333333} - cy={161.4781168} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={281.6} - cy={162.16472064} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={305.06666666666666} - cy={163.21082752} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={328.53333333333336} - cy={166.81319824} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={143.21282560000003} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={375.46666666666664} - cy={164.71169104} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={398.93333333333334} - cy={135.88840304000001} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={422.4} - cy={157.5681224} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={445.8666666666667} - cy={165.65258112} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={469.3333333333333} - cy={165.53588480000002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={492.79999999999995} - cy={147.713644} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={516.2666666666667} - cy={164.06490256} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={539.7333333333333} - cy={164.67332336} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={563.2} - cy={166.4792272} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={586.6666666666667} - cy={152.7591936} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={610.1333333333333} - cy={163.19842032} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={633.6} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={657.0666666666667} - cy={161.37461184} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={680.5333333333333} - cy={111.3825856} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={704} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - </g> - </g> - <g - className="rv-xy-plot__series rv-xy-plot__series--linemark" - > - <path - className="rv-xy-plot__series rv-xy-plot__series--line " - d="M0,208C7.822222222222222,198.0144506122449,15.644444444444442,188.0289012244898,23.466666666666665,188.0289012244898C31.288888888888888,188.0289012244898,39.11111111111111,190.93245866666666,46.93333333333333,190.93245866666666C54.75555555555555,190.93245866666666,62.57777777777778,190.28154649962812,70.4,189.81180675918367C78.22222222222223,189.34206701873921,86.04444444444444,188.114020224,93.86666666666666,188.114020224C101.68888888888888,188.114020224,109.5111111111111,188.75217580408165,117.33333333333333,188.75217580408165C125.15555555555555,188.75217580408165,132.9777777777778,187.7737856411058,140.8,186.9231112C148.62222222222223,186.07243675889418,156.44444444444446,183.64812915744682,164.26666666666668,183.64812915744682C172.0888888888889,183.64812915744682,179.9111111111111,187.40215166647675,187.73333333333332,188.65222657560977C195.55555555555554,189.9023014847428,203.37777777777777,191.1485786122449,211.2,191.1485786122449C219.0222222222222,191.1485786122449,226.84444444444443,187.99938938181816,234.66666666666666,187.99938938181816C242.48888888888888,187.99938938181816,250.3111111111111,192.51163795348836,258.1333333333333,192.51163795348836C265.9555555555556,192.51163795348836,273.77777777777777,186.99252785777776,281.6,186.99252785777776C289.4222222222222,186.99252785777776,297.24444444444447,191.5321727255814,305.06666666666666,191.5321727255814C312.8888888888889,191.5321727255814,320.7111111111111,188.75657926666668,328.53333333333336,188.75657926666668C336.35555555555555,188.75657926666668,344.1777777777778,189.74989696,352,189.74989696C359.8222222222222,189.74989696,367.64444444444445,189.71163744,375.46666666666664,189.6351184C383.2888888888889,189.55859936000002,391.1111111111111,184.25858141935484,398.93333333333334,184.25858141935484C406.75555555555553,184.25858141935484,414.5777777777778,190.2827607652174,422.4,190.2827607652174C430.22222222222223,190.2827607652174,438.0444444444444,189.76271776603772,445.8666666666667,189.76271776603772C453.68888888888887,189.76271776603772,461.5111111111111,191.83746261333334,469.3333333333333,191.83746261333334C477.1555555555555,191.83746261333334,484.97777777777776,187.9456040347826,492.79999999999995,187.9456040347826C500.6222222222222,187.9456040347826,508.4444444444444,188.09594339288537,516.2666666666667,188.3966221090909C524.0888888888888,188.69730082529645,531.9111111111112,191.762533248,539.7333333333333,191.762533248C547.5555555555555,191.762533248,555.3777777777779,191.6625795195817,563.2,191.4626720627451C571.0222222222222,191.2627646059085,578.8444444444445,189.4011021381818,586.6666666666667,189.4011021381818C594.4888888888889,189.4011021381818,602.3111111111111,189.79567013943304,610.1333333333333,190.58480614193547C617.9555555555555,191.3739421444379,625.7777777777778,208,633.6,208C641.4222222222222,208,649.2444444444445,194.91739193977511,657.0666666666667,189.69166496603773C664.8888888888889,184.46593799230038,672.7111111111111,176.64563815757575,680.5333333333333,176.64563815757575C688.3555555555555,176.64563815757575,696.1777777777778,192.32281907878786,704,208" - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - style={ - Object { - "opacity": 1, - "stroke": "#6092c0", - "strokeDasharray": undefined, - "strokeWidth": undefined, - } - } - transform="translate(80,16)" - /> - <g - className="rv-xy-plot__series rv-xy-plot__series--mark " - transform="translate(80,16)" - > - <circle - cx={0} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={23.466666666666665} - cy={188.0289012244898} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={46.93333333333333} - cy={190.93245866666666} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={70.4} - cy={189.81180675918367} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={93.86666666666666} - cy={188.114020224} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={117.33333333333333} - cy={188.75217580408165} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={140.8} - cy={186.9231112} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={164.26666666666668} - cy={183.64812915744682} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={187.73333333333332} - cy={188.65222657560977} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={211.2} - cy={191.1485786122449} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={234.66666666666666} - cy={187.99938938181816} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={258.1333333333333} - cy={192.51163795348836} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={281.6} - cy={186.99252785777776} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={305.06666666666666} - cy={191.5321727255814} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={328.53333333333336} - cy={188.75657926666668} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={189.74989696} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={375.46666666666664} - cy={189.6351184} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={398.93333333333334} - cy={184.25858141935484} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={422.4} - cy={190.2827607652174} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={445.8666666666667} - cy={189.76271776603772} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={469.3333333333333} - cy={191.83746261333334} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={492.79999999999995} - cy={187.9456040347826} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={516.2666666666667} - cy={188.3966221090909} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={539.7333333333333} - cy={191.762533248} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={563.2} - cy={191.4626720627451} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={586.6666666666667} - cy={189.4011021381818} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={610.1333333333333} - cy={190.58480614193547} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={633.6} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={657.0666666666667} - cy={189.69166496603773} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={680.5333333333333} - cy={176.64563815757575} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={704} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - </g> - </g> - </svg> - </div> - </div> - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - /> - </div> - </div> - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - > - <g - className=" rv-voronoi" - > - <path - className="rv-voronoi__cell " - d="M91.7333335,256L91.7333335,16L80,16L80,256Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M91.7333335,16L91.7333335,256L115.19999999999999,256L115.19999999999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M115.19999999999999,16L115.19999999999999,256L138.6666665,256L138.6666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M138.6666665,16L138.6666665,256L162.1333335,256L162.1333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M162.1333335,16L162.1333335,256L185.59999999999997,256L185.59999999999997,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M185.59999999999997,16L185.59999999999997,256L209.0666665,256L209.0666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M209.0666665,16L209.0666665,256L232.53333349999997,256L232.53333349999997,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M232.53333349999997,16L232.53333349999997,256L256,256L256,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M256,16L256,256L279.4666665,256L279.4666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M279.4666665,16L279.4666665,256L302.9333335,256L302.9333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M302.9333335,16L302.9333335,256L326.4,256L326.4,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M326.4,16L326.4,256L349.86666649999995,256L349.86666649999995,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M349.86666649999995,16L349.86666649999995,256L373.3333335,256L373.3333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M373.3333335,16L373.3333335,256L396.79999999999995,256L396.79999999999995,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M396.79999999999995,16L396.79999999999995,256L420.2666665,256L420.2666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M420.2666665,16L420.2666665,256L443.73333349999996,256L443.73333349999996,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M443.73333349999996,16L443.73333349999996,256L467.2,256L467.2,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M467.2,16L467.2,256L490.6666665,256L490.6666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M490.6666665,16L490.6666665,256L514.1333334999999,256L514.1333334999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M514.1333334999999,16L514.1333334999999,256L537.5999999999999,256L537.5999999999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M537.5999999999999,16L537.5999999999999,256L561.0666664999999,256L561.0666664999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M561.0666664999999,16L561.0666664999999,256L584.5333335,256L584.5333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M584.5333335,16L584.5333335,256L608,256L608,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M608,16L608,256L631.4666665,256L631.4666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M631.4666665,16L631.4666665,256L654.9333334999999,256L654.9333334999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M654.9333334999999,16L654.9333334999999,256L678.4,256L678.4,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M678.4,16L678.4,256L701.8666665000001,256L701.8666665000001,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M701.8666665000001,16L701.8666665000001,256L725.3333335,256L725.3333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M725.3333335,16L725.3333335,256L748.8,256L748.8,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M748.8,16L748.8,256L772.2666664999999,256L772.2666664999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M772.2666664999999,16L772.2666664999999,256L800,256L800,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - </g> - </svg> - </div> - </div> - </div>, - .c1 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - font-size: 12px; - color: #69707d; - cursor: pointer; - opacity: 1; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.c2 { - width: 11px; - height: 11px; - margin-right: 5.5px; - background: #6092c0; - border-radius: 100%; -} - -.c5 { - width: 11px; - height: 11px; - margin-right: 5.5px; - background: #d6bf57; - border-radius: 100%; -} - -.c6 { - width: 11px; - height: 11px; - margin-right: 5.5px; - background: #da8b45; - border-radius: 100%; -} - -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - margin-left: 80px; - -webkit-flex-wrap: wrap; - -ms-flex-wrap: wrap; - flex-wrap: wrap; -} - -.c0 > div { - margin-top: 8px; - margin-right: 16px; -} - -.c0 > div:last-child { - margin-right: 0; -} - -.c3 { - white-space: nowrap; - color: #98a2b3; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} - -.c4 { - margin-left: 4px; - color: #000000; - display: inline-block; -} - -<styled.div> - <div - className="c0" - > - <styled.div - clickable={true} - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <div - className="c1" - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <styled.span - color="#6092c0" - radius={11} - shape="circle" - withMargin={true} - > - <span - className="c2" - color="#6092c0" - radius={11} - shape="circle" - /> - </styled.span> - <styled.span> - <span - className="c3" - > - Avg. - <styled.span> - <span - className="c4" - > - 468 ms - </span> - </styled.span> - </span> - </styled.span> - </div> - </styled.div> - <styled.div - clickable={true} - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <div - className="c1" - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <styled.span - color="#d6bf57" - radius={11} - shape="circle" - withMargin={true} - > - <span - className="c5" - color="#d6bf57" - radius={11} - shape="circle" - /> - </styled.span> - <styled.span> - <span - className="c3" - > - 95th percentile - </span> - </styled.span> - </div> - </styled.div> - <styled.div - clickable={true} - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <div - className="c1" - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <styled.span - color="#da8b45" - radius={11} - shape="circle" - withMargin={true} - > - <span - className="c6" - color="#da8b45" - radius={11} - shape="circle" - /> - </styled.span> - <styled.span> - <span - className="c3" - > - 99th percentile - </span> - </styled.span> - </div> - </styled.div> - </div> - </styled.div>, -] -`; - -exports[`when response has data Initially should have correct state 1`] = ` -Object { - "isDrawing": false, - "selectionEnd": null, - "selectionStart": null, - "seriesEnabledState": Array [], - "showAnnotations": true, -} -`; - -exports[`when response has data when dragging without releasing should display SelectionMarker 1`] = ` -<rect - fill="black" - fillOpacity="0.1" - height={208} - pointerEvents="none" - width={234.66666666666663} - x={314.66666666666663} - y={16} -/> -`; - -exports[`when response has data when setting hoverX should display tooltip 1`] = ` -Array [ - Object { - "color": "#6092c0", - "text": "Avg.", - "value": 438704.4, - }, - Object { - "color": "#d6bf57", - "text": "95th", - "value": 1557383.999999999, - }, - Object { - "color": "#da8b45", - "text": "99th", - "value": 1820377.1200000006, - }, -] -`; - -exports[`when response has data when setting hoverX should match snapshots 1`] = ` -Array [ - .c5 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - font-size: 12px; - color: #69707d; - cursor: initial; - opacity: 1; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.c6 { - width: 8px; - height: 8px; - margin-right: 4px; - background: #6092c0; - border-radius: 100%; -} - -.c8 { - width: 8px; - height: 8px; - margin-right: 4px; - background: #d6bf57; - border-radius: 100%; -} - -.c9 { - width: 8px; - height: 8px; - margin-right: 4px; - background: #da8b45; - border-radius: 100%; -} - -.c0 { - margin: 0 16px; - -webkit-transform: translateY(-50%); - -ms-transform: translateY(-50%); - transform: translateY(-50%); - border: 1px solid #d3dae6; - background: #ffffff; - border-radius: 4px; - font-size: 14px; - color: #000000; -} - -.c1 { - background: #f5f7fa; - border-bottom: 1px solid #d3dae6; - border-radius: 4px 4px 0 0; - padding: 8px; - color: #98a2b3; -} - -.c2 { - margin: 8px; - margin-right: 16px; - font-size: 12px; -} - -.c10 { - color: #98a2b3; - margin: 8px; - font-size: 12px; -} - -.c3 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - margin-bottom: 4px; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; -} - -.c4 { - color: #98a2b3; - padding-bottom: 0; - padding-right: 8px; -} - -.c7 { - color: #69707d; - font-size: 14px; -} - -<div - style={ - Object { - "height": 256, - "position": "relative", - } - } - > - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - > - <g - className="rv-xy-plot__axis rv-xy-plot__axis--horizontal " - style={Object {}} - transform="translate(80,224)" - > - <line - className="rv-xy-plot__axis__line" - style={Object {}} - x1={0} - x2={704} - y1={0} - y2={0} - /> - <g - className="rv-xy-plot__axis__ticks" - transform="translate(0, 0)" - > - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(70.4, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283000000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(187.73333333333332, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283300000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(305.06666666666666, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283600000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(422.4, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502283900000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(539.7333333333333, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502284200000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(657.0666666666667, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1502284500000 - </text> - </g> - </g> - </g> - <g - className="rv-xy-plot__grid-lines" - transform="translate(80,16)" - > - <line - className="rv-xy-plot__grid-lines__line" - x1={0} - x2={704} - y1={208} - y2={208} - /> - <line - className="rv-xy-plot__grid-lines__line" - x1={0} - x2={704} - y1={104} - y2={104} - /> - <line - className="rv-xy-plot__grid-lines__line" - x1={0} - x2={704} - y1={0} - y2={0} - /> - </g> - <g - className="rv-xy-plot__axis rv-xy-plot__axis--vertical " - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0,16)" - > - <line - className="rv-xy-plot__axis__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={80} - x2={80} - y1={0} - y2={208} - /> - <g - className="rv-xy-plot__axis__ticks" - transform="translate(80, 0)" - > - <g - className="rv-xy-plot__axis__tick" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0, 208)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={0} - x2={-0} - y1={0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.32em" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - textAnchor="end" - transform="translate(-8, 0)" - > - 0 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0, 104)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={0} - x2={-0} - y1={0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.32em" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - textAnchor="end" - transform="translate(-8, 0)" - > - 2500000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - transform="translate(0, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={ - Object { - "fill": "none", - "line": Object { - "fill": "none", - "stroke": "none", - }, - "stroke": "none", - } - } - x1={0} - x2={-0} - y1={0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.32em" - style={ - Object { - "line": Object { - "fill": "none", - "stroke": "none", - }, - } - } - textAnchor="end" - transform="translate(-8, 0)" - > - 5000000 - </text> - </g> - </g> - </g> - <g - className="rv-xy-plot__series rv-xy-plot__series--linemark" - > - <path - className="rv-xy-plot__series rv-xy-plot__series--line " - d="M0,208C7.822222222222222,177.67069216000004,15.644444444444442,147.3413843200001,23.466666666666665,145.82854374400006C31.288888888888888,144.31570316800003,39.11111111111111,143.55928288,46.93333333333333,143.55928288C54.75555555555555,143.55928288,62.57777777777778,143.69173256533335,70.4,143.95663193600006C78.22222222222223,144.22153130666678,86.04444444444444,150.087546752,93.86666666666666,150.087546752C101.68888888888888,150.087546752,109.5111111111111,143.61986468266673,117.33333333333333,141.36762432000006C125.15555555555555,139.1153839573334,132.9777777777778,138.10678017066667,140.8,136.574104576C148.62222222222223,135.04142898133333,156.44444444444446,134.60761100800002,164.26666666666668,132.171570752C172.0888888888889,129.735530496,179.9111111111111,122.7740017919999,187.73333333333332,121.95786303999996C195.55555555555554,121.14172428800003,203.37777777777777,120.73365491200006,211.2,120.73365491200006C219.0222222222222,120.73365491200006,226.84444444444443,150.33875334399997,234.66666666666666,150.33875334399997C242.48888888888888,150.33875334399997,250.3111111111111,145.21265574400005,258.1333333333333,145.21265574400005C265.9555555555556,145.21265574400005,273.77777777777777,159.49950515199998,281.6,159.49950515199998C289.4222222222222,159.49950515199998,297.24444444444447,159.25604087466667,305.06666666666666,158.76911232C312.8888888888889,158.28218376533334,320.7111111111111,153.13340861866664,328.53333333333336,148.71727519999996C336.35555555555555,144.30114178133329,344.1777777777778,136.23707191466664,352,132.27231180799998C359.8222222222222,128.30755170133332,367.64444444444445,127.55802467199999,375.46666666666664,124.92871456C383.2888888888889,122.299404448,391.1111111111111,116.49645113600002,398.93333333333334,116.49645113600002C406.75555555555553,116.49645113600002,414.5777777777778,142.8818553066667,422.4,147.94231920000001C430.22222222222223,153.00278309333333,438.0444444444444,155.53301504,445.8666666666667,155.53301504C453.68888888888887,155.53301504,461.5111111111111,147.5484465173333,469.3333333333333,141.915090304C477.1555555555555,136.28173409066667,484.97777777777776,121.73287776000008,492.79999999999995,121.73287776000008C500.6222222222222,121.73287776000008,508.4444444444444,147.90280169599995,516.2666666666667,152.63684758399998C524.0888888888888,157.370893472,531.9111111111112,159.73791641600002,539.7333333333333,159.73791641600002C547.5555555555555,159.73791641600002,555.3777777777779,140.4438672,563.2,140.4438672C571.0222222222222,140.4438672,578.8444444444445,150.146582976,586.6666666666667,150.146582976C594.4888888888889,150.146582976,602.3111111111111,121.98686448,610.1333333333333,121.98686448C617.9555555555555,121.98686448,625.7777777777778,208,633.6,208C641.4222222222222,208,649.2444444444445,133.8337757120002,657.0666666666667,102.0323582720003C664.8888888888889,70.23094083200053,672.7111111111111,17.191495360000836,680.5333333333333,17.191495360000836C688.3555555555555,17.191495360000836,696.1777777777778,112.59574768000043,704,208" - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - style={ - Object { - "opacity": 1, - "stroke": "#da8b45", - "strokeDasharray": undefined, - "strokeWidth": undefined, - } - } - transform="translate(80,16)" - /> - <g - className="rv-xy-plot__series rv-xy-plot__series--mark " - transform="translate(80,16)" - > - <circle - cx={0} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={23.466666666666665} - cy={145.82854374400006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={46.93333333333333} - cy={143.55928288} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={70.4} - cy={143.95663193600006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={93.86666666666666} - cy={150.087546752} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={117.33333333333333} - cy={141.36762432000006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={140.8} - cy={136.574104576} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={164.26666666666668} - cy={132.171570752} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={187.73333333333332} - cy={121.95786303999996} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={211.2} - cy={120.73365491200006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={234.66666666666666} - cy={150.33875334399997} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={258.1333333333333} - cy={145.21265574400005} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={281.6} - cy={159.49950515199998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={305.06666666666666} - cy={158.76911232} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={328.53333333333336} - cy={148.71727519999996} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={132.27231180799998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={375.46666666666664} - cy={124.92871456} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={398.93333333333334} - cy={116.49645113600002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={422.4} - cy={147.94231920000001} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={445.8666666666667} - cy={155.53301504} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={469.3333333333333} - cy={141.915090304} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={492.79999999999995} - cy={121.73287776000008} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={516.2666666666667} - cy={152.63684758399998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={539.7333333333333} - cy={159.73791641600002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={563.2} - cy={140.4438672} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={586.6666666666667} - cy={150.146582976} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={610.1333333333333} - cy={121.98686448} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={633.6} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={657.0666666666667} - cy={102.0323582720003} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={680.5333333333333} - cy={17.191495360000836} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={704} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - </g> - </g> - <g - className="rv-xy-plot__series rv-xy-plot__series--linemark" - > - <path - className="rv-xy-plot__series rv-xy-plot__series--line " - d="M0,208C7.822222222222222,183.10434549333334,15.644444444444442,158.20869098666665,23.466666666666665,157.4191424C31.288888888888888,156.62959381333334,39.11111111111111,156.23481952,46.93333333333333,156.23481952C54.75555555555555,156.23481952,62.57777777777778,160.81596682666668,70.4,161.56425792000002C78.22222222222223,162.31254901333335,86.04444444444444,162.68669456,93.86666666666666,162.68669456C101.68888888888888,162.68669456,109.5111111111111,158.86059904000004,117.33333333333333,158.86059904000004C125.15555555555555,158.86059904000004,132.9777777777778,163.62246992000001,140.8,163.62246992000001C148.62222222222223,163.62246992000001,156.44444444444446,142.73391392000002,164.26666666666668,142.73391392000002C172.0888888888889,142.73391392000002,179.9111111111111,165.8699744,187.73333333333332,165.8699744C195.55555555555554,165.8699744,203.37777777777777,163.6534529066668,211.2,163.52439168000006C219.0222222222222,163.39533045333334,226.84444444444443,163.45986106666672,234.66666666666666,163.33079984C242.48888888888888,163.20173861333328,250.3111111111111,161.4781168,258.1333333333333,161.4781168C265.9555555555556,161.4781168,273.77777777777777,161.87593552,281.6,162.16472064C289.4222222222222,162.45350576,297.24444444444447,162.51342293333335,305.06666666666666,163.21082752C312.8888888888889,163.90823210666667,320.7111111111111,166.81319824,328.53333333333336,166.81319824C336.35555555555555,166.81319824,344.1777777777778,143.21282560000003,352,143.21282560000003C359.8222222222222,143.21282560000003,367.64444444444445,164.71169104,375.46666666666664,164.71169104C383.2888888888889,164.71169104,391.1111111111111,135.88840304000001,398.93333333333334,135.88840304000001C406.75555555555553,135.88840304000001,414.5777777777778,152.6074260533333,422.4,157.5681224C430.22222222222223,162.52881874666667,438.0444444444444,165.65258112,445.8666666666667,165.65258112C453.68888888888887,165.65258112,461.5111111111111,165.61368234666668,469.3333333333333,165.53588480000002C477.1555555555555,165.45808725333336,484.97777777777776,147.713644,492.79999999999995,147.713644C500.6222222222222,147.713644,508.4444444444444,163.65928869333334,516.2666666666667,164.06490256C524.0888888888888,164.47051642666668,531.9111111111112,164.27093592,539.7333333333333,164.67332336C547.5555555555555,165.07571080000002,555.3777777777779,166.4792272,563.2,166.4792272C571.0222222222222,166.4792272,578.8444444444445,152.7591936,586.6666666666667,152.7591936C594.4888888888889,152.7591936,602.3111111111111,156.23893584,610.1333333333333,163.19842032C617.9555555555555,170.15790480000004,625.7777777777778,208,633.6,208C641.4222222222222,208,649.2444444444445,177.47751424000003,657.0666666666667,161.37461184C664.8888888888889,145.27170944000005,672.7111111111111,111.3825856,680.5333333333333,111.3825856C688.3555555555555,111.3825856,696.1777777777778,159.69129279999999,704,208" - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - style={ - Object { - "opacity": 1, - "stroke": "#d6bf57", - "strokeDasharray": undefined, - "strokeWidth": undefined, - } - } - transform="translate(80,16)" - /> - <g - className="rv-xy-plot__series rv-xy-plot__series--mark " - transform="translate(80,16)" - > - <circle - cx={0} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={23.466666666666665} - cy={157.4191424} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={46.93333333333333} - cy={156.23481952} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={70.4} - cy={161.56425792000002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={93.86666666666666} - cy={162.68669456} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={117.33333333333333} - cy={158.86059904000004} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={140.8} - cy={163.62246992000001} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={164.26666666666668} - cy={142.73391392000002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={187.73333333333332} - cy={165.8699744} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={211.2} - cy={163.52439168000006} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={234.66666666666666} - cy={163.33079984} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={258.1333333333333} - cy={161.4781168} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={281.6} - cy={162.16472064} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={305.06666666666666} - cy={163.21082752} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={328.53333333333336} - cy={166.81319824} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={143.21282560000003} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={375.46666666666664} - cy={164.71169104} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={398.93333333333334} - cy={135.88840304000001} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={422.4} - cy={157.5681224} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={445.8666666666667} - cy={165.65258112} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={469.3333333333333} - cy={165.53588480000002} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={492.79999999999995} - cy={147.713644} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={516.2666666666667} - cy={164.06490256} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={539.7333333333333} - cy={164.67332336} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={563.2} - cy={166.4792272} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={586.6666666666667} - cy={152.7591936} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={610.1333333333333} - cy={163.19842032} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={633.6} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={657.0666666666667} - cy={161.37461184} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={680.5333333333333} - cy={111.3825856} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={704} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - </g> - </g> - <g - className="rv-xy-plot__series rv-xy-plot__series--linemark" - > - <path - className="rv-xy-plot__series rv-xy-plot__series--line " - d="M0,208C7.822222222222222,198.0144506122449,15.644444444444442,188.0289012244898,23.466666666666665,188.0289012244898C31.288888888888888,188.0289012244898,39.11111111111111,190.93245866666666,46.93333333333333,190.93245866666666C54.75555555555555,190.93245866666666,62.57777777777778,190.28154649962812,70.4,189.81180675918367C78.22222222222223,189.34206701873921,86.04444444444444,188.114020224,93.86666666666666,188.114020224C101.68888888888888,188.114020224,109.5111111111111,188.75217580408165,117.33333333333333,188.75217580408165C125.15555555555555,188.75217580408165,132.9777777777778,187.7737856411058,140.8,186.9231112C148.62222222222223,186.07243675889418,156.44444444444446,183.64812915744682,164.26666666666668,183.64812915744682C172.0888888888889,183.64812915744682,179.9111111111111,187.40215166647675,187.73333333333332,188.65222657560977C195.55555555555554,189.9023014847428,203.37777777777777,191.1485786122449,211.2,191.1485786122449C219.0222222222222,191.1485786122449,226.84444444444443,187.99938938181816,234.66666666666666,187.99938938181816C242.48888888888888,187.99938938181816,250.3111111111111,192.51163795348836,258.1333333333333,192.51163795348836C265.9555555555556,192.51163795348836,273.77777777777777,186.99252785777776,281.6,186.99252785777776C289.4222222222222,186.99252785777776,297.24444444444447,191.5321727255814,305.06666666666666,191.5321727255814C312.8888888888889,191.5321727255814,320.7111111111111,188.75657926666668,328.53333333333336,188.75657926666668C336.35555555555555,188.75657926666668,344.1777777777778,189.74989696,352,189.74989696C359.8222222222222,189.74989696,367.64444444444445,189.71163744,375.46666666666664,189.6351184C383.2888888888889,189.55859936000002,391.1111111111111,184.25858141935484,398.93333333333334,184.25858141935484C406.75555555555553,184.25858141935484,414.5777777777778,190.2827607652174,422.4,190.2827607652174C430.22222222222223,190.2827607652174,438.0444444444444,189.76271776603772,445.8666666666667,189.76271776603772C453.68888888888887,189.76271776603772,461.5111111111111,191.83746261333334,469.3333333333333,191.83746261333334C477.1555555555555,191.83746261333334,484.97777777777776,187.9456040347826,492.79999999999995,187.9456040347826C500.6222222222222,187.9456040347826,508.4444444444444,188.09594339288537,516.2666666666667,188.3966221090909C524.0888888888888,188.69730082529645,531.9111111111112,191.762533248,539.7333333333333,191.762533248C547.5555555555555,191.762533248,555.3777777777779,191.6625795195817,563.2,191.4626720627451C571.0222222222222,191.2627646059085,578.8444444444445,189.4011021381818,586.6666666666667,189.4011021381818C594.4888888888889,189.4011021381818,602.3111111111111,189.79567013943304,610.1333333333333,190.58480614193547C617.9555555555555,191.3739421444379,625.7777777777778,208,633.6,208C641.4222222222222,208,649.2444444444445,194.91739193977511,657.0666666666667,189.69166496603773C664.8888888888889,184.46593799230038,672.7111111111111,176.64563815757575,680.5333333333333,176.64563815757575C688.3555555555555,176.64563815757575,696.1777777777778,192.32281907878786,704,208" - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - style={ - Object { - "opacity": 1, - "stroke": "#6092c0", - "strokeDasharray": undefined, - "strokeWidth": undefined, - } - } - transform="translate(80,16)" - /> - <g - className="rv-xy-plot__series rv-xy-plot__series--mark " - transform="translate(80,16)" - > - <circle - cx={0} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={23.466666666666665} - cy={188.0289012244898} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={46.93333333333333} - cy={190.93245866666666} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={70.4} - cy={189.81180675918367} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={93.86666666666666} - cy={188.114020224} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={117.33333333333333} - cy={188.75217580408165} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={140.8} - cy={186.9231112} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={164.26666666666668} - cy={183.64812915744682} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={187.73333333333332} - cy={188.65222657560977} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={211.2} - cy={191.1485786122449} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={234.66666666666666} - cy={187.99938938181816} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={258.1333333333333} - cy={192.51163795348836} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={281.6} - cy={186.99252785777776} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={305.06666666666666} - cy={191.5321727255814} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={328.53333333333336} - cy={188.75657926666668} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={189.74989696} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={375.46666666666664} - cy={189.6351184} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={398.93333333333334} - cy={184.25858141935484} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={422.4} - cy={190.2827607652174} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={445.8666666666667} - cy={189.76271776603772} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={469.3333333333333} - cy={191.83746261333334} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={492.79999999999995} - cy={187.9456040347826} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={516.2666666666667} - cy={188.3966221090909} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={539.7333333333333} - cy={191.762533248} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={563.2} - cy={191.4626720627451} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={586.6666666666667} - cy={189.4011021381818} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={610.1333333333333} - cy={190.58480614193547} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={633.6} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={657.0666666666667} - cy={189.69166496603773} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={680.5333333333333} - cy={176.64563815757575} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - <circle - cx={704} - cy={208} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={1} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - </g> - </g> - </svg> - </div> - </div> - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - > - <g - className="rv-xy-plot__series rv-xy-plot__series--mark undefined" - transform="translate(80,16)" - > - <circle - cx={352} - cy={132.27231180799998} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={5} - style={ - Object { - "fill": "#da8b45", - "opacity": 1, - "stroke": "#da8b45", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={143.21282560000003} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={5} - style={ - Object { - "fill": "#d6bf57", - "opacity": 1, - "stroke": "#d6bf57", - "strokeWidth": 1, - } - } - /> - <circle - cx={352} - cy={189.74989696} - onClick={[Function]} - onContextMenu={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - r={5} - style={ - Object { - "fill": "#6092c0", - "opacity": 1, - "stroke": "#6092c0", - "strokeWidth": 1, - } - } - /> - </g> - <g - className="rv-xy-plot__grid-lines" - transform="translate(80,16)" - > - <line - className="rv-xy-plot__grid-lines__line" - x1={352} - x2={352} - y1={0} - y2={208} - /> - </g> - </svg> - <div - className="rv-hint rv-hint--horizontalAlign-right - rv-hint--verticalAlign-bottom" - style={ - Object { - "left": 432, - "position": "absolute", - "top": 120, - } - } - > - <styled.div> - <div - className="c0" - > - <styled.div> - <div - className="c1" - > - 1502283720 - </div> - </styled.div> - <styled.div> - <div - className="c2" - > - <styled.div> - <div - className="c3" - > - <Styled(Legend) - color="#6092c0" - radius={8} - text="Avg." - > - <styled.div - className="c4" - clickable={false} - disabled={false} - fontSize="12px" - > - <div - className="c5 c4" - disabled={false} - fontSize="12px" - > - <styled.span - color="#6092c0" - radius={8} - shape="circle" - withMargin={true} - > - <span - className="c6" - color="#6092c0" - radius={8} - shape="circle" - /> - </styled.span> - Avg. - </div> - </styled.div> - </Styled(Legend)> - <styled.div> - <div - className="c7" - > - 438704.4 - </div> - </styled.div> - </div> - </styled.div> - <styled.div> - <div - className="c3" - > - <Styled(Legend) - color="#d6bf57" - radius={8} - text="95th" - > - <styled.div - className="c4" - clickable={false} - disabled={false} - fontSize="12px" - > - <div - className="c5 c4" - disabled={false} - fontSize="12px" - > - <styled.span - color="#d6bf57" - radius={8} - shape="circle" - withMargin={true} - > - <span - className="c8" - color="#d6bf57" - radius={8} - shape="circle" - /> - </styled.span> - 95th - </div> - </styled.div> - </Styled(Legend)> - <styled.div> - <div - className="c7" - > - 1557383.999999999 - </div> - </styled.div> - </div> - </styled.div> - <styled.div> - <div - className="c3" - > - <Styled(Legend) - color="#da8b45" - radius={8} - text="99th" - > - <styled.div - className="c4" - clickable={false} - disabled={false} - fontSize="12px" - > - <div - className="c5 c4" - disabled={false} - fontSize="12px" - > - <styled.span - color="#da8b45" - radius={8} - shape="circle" - withMargin={true} - > - <span - className="c9" - color="#da8b45" - radius={8} - shape="circle" - /> - </styled.span> - 99th - </div> - </styled.div> - </Styled(Legend)> - <styled.div> - <div - className="c7" - > - 1820377.1200000006 - </div> - </styled.div> - </div> - </styled.div> - </div> - </styled.div> - <styled.div> - <div - className="c10" - /> - </styled.div> - </div> - </styled.div> - </div> - </div> - </div> - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - > - <g - className=" rv-voronoi" - > - <path - className="rv-voronoi__cell " - d="M91.7333335,256L91.7333335,16L80,16L80,256Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M91.7333335,16L91.7333335,256L115.19999999999999,256L115.19999999999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M115.19999999999999,16L115.19999999999999,256L138.6666665,256L138.6666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M138.6666665,16L138.6666665,256L162.1333335,256L162.1333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M162.1333335,16L162.1333335,256L185.59999999999997,256L185.59999999999997,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M185.59999999999997,16L185.59999999999997,256L209.0666665,256L209.0666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M209.0666665,16L209.0666665,256L232.53333349999997,256L232.53333349999997,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M232.53333349999997,16L232.53333349999997,256L256,256L256,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M256,16L256,256L279.4666665,256L279.4666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M279.4666665,16L279.4666665,256L302.9333335,256L302.9333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M302.9333335,16L302.9333335,256L326.4,256L326.4,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M326.4,16L326.4,256L349.86666649999995,256L349.86666649999995,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M349.86666649999995,16L349.86666649999995,256L373.3333335,256L373.3333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M373.3333335,16L373.3333335,256L396.79999999999995,256L396.79999999999995,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M396.79999999999995,16L396.79999999999995,256L420.2666665,256L420.2666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M420.2666665,16L420.2666665,256L443.73333349999996,256L443.73333349999996,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M443.73333349999996,16L443.73333349999996,256L467.2,256L467.2,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M467.2,16L467.2,256L490.6666665,256L490.6666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M490.6666665,16L490.6666665,256L514.1333334999999,256L514.1333334999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M514.1333334999999,16L514.1333334999999,256L537.5999999999999,256L537.5999999999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M537.5999999999999,16L537.5999999999999,256L561.0666664999999,256L561.0666664999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M561.0666664999999,16L561.0666664999999,256L584.5333335,256L584.5333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M584.5333335,16L584.5333335,256L608,256L608,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M608,16L608,256L631.4666665,256L631.4666665,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M631.4666665,16L631.4666665,256L654.9333334999999,256L654.9333334999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M654.9333334999999,16L654.9333334999999,256L678.4,256L678.4,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M678.4,16L678.4,256L701.8666665000001,256L701.8666665000001,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M701.8666665000001,16L701.8666665000001,256L725.3333335,256L725.3333335,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M725.3333335,16L725.3333335,256L748.8,256L748.8,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M748.8,16L748.8,256L772.2666664999999,256L772.2666664999999,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - <path - className="rv-voronoi__cell " - d="M772.2666664999999,16L772.2666664999999,256L800,256L800,16Z" - fill="none" - onClick={[Function]} - onMouseDown={[Function]} - onMouseOut={[Function]} - onMouseOver={[Function]} - onMouseUp={[Function]} - style={ - Object { - "pointerEvents": "all", - } - } - /> - </g> - </svg> - </div> - </div> - </div>, - .c1 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - font-size: 12px; - color: #69707d; - cursor: pointer; - opacity: 1; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.c2 { - width: 11px; - height: 11px; - margin-right: 5.5px; - background: #6092c0; - border-radius: 100%; -} - -.c5 { - width: 11px; - height: 11px; - margin-right: 5.5px; - background: #d6bf57; - border-radius: 100%; -} - -.c6 { - width: 11px; - height: 11px; - margin-right: 5.5px; - background: #da8b45; - border-radius: 100%; -} - -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - margin-left: 80px; - -webkit-flex-wrap: wrap; - -ms-flex-wrap: wrap; - flex-wrap: wrap; -} - -.c0 > div { - margin-top: 8px; - margin-right: 16px; -} - -.c0 > div:last-child { - margin-right: 0; -} - -.c3 { - white-space: nowrap; - color: #98a2b3; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} - -.c4 { - margin-left: 4px; - color: #000000; - display: inline-block; -} - -<styled.div> - <div - className="c0" - > - <styled.div - clickable={true} - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <div - className="c1" - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <styled.span - color="#6092c0" - radius={11} - shape="circle" - withMargin={true} - > - <span - className="c2" - color="#6092c0" - radius={11} - shape="circle" - /> - </styled.span> - <styled.span> - <span - className="c3" - > - Avg. - <styled.span> - <span - className="c4" - > - 468 ms - </span> - </styled.span> - </span> - </styled.span> - </div> - </styled.div> - <styled.div - clickable={true} - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <div - className="c1" - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <styled.span - color="#d6bf57" - radius={11} - shape="circle" - withMargin={true} - > - <span - className="c5" - color="#d6bf57" - radius={11} - shape="circle" - /> - </styled.span> - <styled.span> - <span - className="c3" - > - 95th percentile - </span> - </styled.span> - </div> - </styled.div> - <styled.div - clickable={true} - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <div - className="c1" - disabled={false} - fontSize="12px" - onClick={[Function]} - > - <styled.span - color="#da8b45" - radius={11} - shape="circle" - withMargin={true} - > - <span - className="c6" - color="#da8b45" - radius={11} - shape="circle" - /> - </styled.span> - <styled.span> - <span - className="c3" - > - 99th percentile - </span> - </styled.span> - </div> - </styled.div> - </div> - </styled.div>, -] -`; - -exports[`when response has data when setting hoverX should match snapshots 2`] = ` -Object { - "isDrawing": false, - "selectionEnd": null, - "selectionStart": null, - "seriesEnabledState": Array [], - "showAnnotations": true, -} -`; - -exports[`when response has no data Initially should have correct markup 1`] = ` -Array [ - <div - style={ - Object { - "height": 256, - "position": "relative", - } - } - > - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - > - <g - className="rv-xy-plot__axis rv-xy-plot__axis--horizontal " - style={Object {}} - transform="translate(80,224)" - > - <line - className="rv-xy-plot__axis__line" - style={Object {}} - x1={0} - x2={704} - y1={0} - y2={0} - /> - <g - className="rv-xy-plot__axis__ticks" - transform="translate(0, 0)" - > - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(0, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451606400000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(58.666666666666664, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451606700000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(117.33333333333333, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451607000000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(176, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451607300000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(234.66666666666666, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451607600000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(293.33333333333337, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451607900000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(352, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451608200000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(410.6666666666667, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451608500000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(469.3333333333333, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451608800000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(528, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451609100000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(586.6666666666667, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451609400000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(645.3333333333333, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451609700000 - </text> - </g> - <g - className="rv-xy-plot__axis__tick" - style={Object {}} - transform="translate(704, 0)" - > - <line - className="rv-xy-plot__axis__tick__line" - style={Object {}} - x1={0} - x2={0} - y1={-0} - y2={0} - /> - <text - className="rv-xy-plot__axis__tick__text" - dy="0.72em" - style={Object {}} - textAnchor="middle" - transform="translate(0, 8)" - > - 1451610000000 - </text> - </g> - </g> - </g> - </svg> - <div - style={ - Object { - "left": "50%", - "position": "absolute", - "top": "50%", - "transform": "translate(calc(-50% + 14px),calc(-50% + -16px - 15px))", - } - } - > - No data within this time range. - </div> - </div> - </div> - <div - style={ - Object { - "left": 0, - "pointerEvents": "none", - "position": "absolute", - "top": 0, - } - } - > - <div - className="rv-xy-plot " - style={ - Object { - "height": "256px", - "width": "800px", - } - } - > - <svg - className="rv-xy-plot__inner" - height={256} - onClick={[Function]} - onDoubleClick={[Function]} - onMouseDown={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} - onMouseMove={[Function]} - onWheel={[Function]} - width={800} - /> - </div> - </div> - </div>, - "", -] -`; - -exports[`when response has no data Initially should have correct state 1`] = ` -Object { - "isDrawing": false, - "selectionEnd": null, - "selectionStart": null, - "seriesEnabledState": Array [], - "showAnnotations": true, -} -`; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json deleted file mode 100644 index e8b96b501af0f..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "responseTimes": { - "avg": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 480074.48979591834 }, - { "x": 1502282940000, "y": 410277.4358974359 }, - { "x": 1502283000000, "y": 437216.1836734694 }, - { "x": 1502283060000, "y": 478028.36 }, - { "x": 1502283120000, "y": 462688.0816326531 }, - { "x": 1502283180000, "y": 506655.98076923075 }, - { "x": 1502283240000, "y": 585381.5106382979 }, - { "x": 1502283300000, "y": 465090.7073170732 }, - { "x": 1502283360000, "y": 405082.2448979592 }, - { "x": 1502283420000, "y": 480783.9090909091 }, - { "x": 1502283480000, "y": 372316.3953488372 }, - { "x": 1502283540000, "y": 504987.31111111114 }, - { "x": 1502283600000, "y": 395861.23255813954 }, - { "x": 1502283660000, "y": 462582.2291666667 }, - { "x": 1502283720000, "y": 438704.4 }, - { "x": 1502283780000, "y": 441463.5 }, - { "x": 1502283840000, "y": 570707.1774193548 }, - { "x": 1502283900000, "y": 425895.17391304346 }, - { "x": 1502283960000, "y": 438396.2075471698 }, - { "x": 1502284020000, "y": 388522.5333333333 }, - { "x": 1502284080000, "y": 482076.82608695654 }, - { "x": 1502284140000, "y": 471235.04545454547 }, - { "x": 1502284200000, "y": 390323.72 }, - { "x": 1502284260000, "y": 397531.92156862747 }, - { "x": 1502284320000, "y": 447088.89090909093 }, - { "x": 1502284380000, "y": 418634.46774193546 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 440104.2075471698 }, - { "x": 1502284560000, "y": 753710.6212121212 }, - { "x": 1502284620000, "y": 0 } - ], - "p95": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 1215886 }, - { "x": 1502282940000, "y": 1244355.3000000003 }, - { "x": 1502283000000, "y": 1116243.7999999993 }, - { "x": 1502283060000, "y": 1089262.15 }, - { "x": 1502283120000, "y": 1181235.599999999 }, - { "x": 1502283180000, "y": 1066767.5499999998 }, - { "x": 1502283240000, "y": 1568896.2999999996 }, - { "x": 1502283300000, "y": 1012741 }, - { "x": 1502283360000, "y": 1069125.1999999988 }, - { "x": 1502283420000, "y": 1073778.85 }, - { "x": 1502283480000, "y": 1118314.4999999998 }, - { "x": 1502283540000, "y": 1101809.5999999999 }, - { "x": 1502283600000, "y": 1076662.7999999998 }, - { "x": 1502283660000, "y": 990067.35 }, - { "x": 1502283720000, "y": 1557383.999999999 }, - { "x": 1502283780000, "y": 1040584.3500000001 }, - { "x": 1502283840000, "y": 1733451.8499999994 }, - { "x": 1502283900000, "y": 1212304.75 }, - { "x": 1502283960000, "y": 1017966.8 }, - { "x": 1502284020000, "y": 1020771.9999999999 }, - { "x": 1502284080000, "y": 1449191.25 }, - { "x": 1502284140000, "y": 1056132.15 }, - { "x": 1502284200000, "y": 1041506.6499999998 }, - { "x": 1502284260000, "y": 998095.5 }, - { "x": 1502284320000, "y": 1327904 }, - { "x": 1502284380000, "y": 1076961.05 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 1120802.5999999999 }, - { "x": 1502284560000, "y": 2322534 }, - { "x": 1502284620000, "y": 0 } - ], - "p99": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 1494506.1599999988 }, - { "x": 1502282940000, "y": 1549055.6999999993 }, - { "x": 1502283000000, "y": 1539504.0399999986 }, - { "x": 1502283060000, "y": 1392126.2799999996 }, - { "x": 1502283120000, "y": 1601739.799999998 }, - { "x": 1502283180000, "y": 1716968.6400000001 }, - { "x": 1502283240000, "y": 1822798.7799999998 }, - { "x": 1502283300000, "y": 2068320.600000001 }, - { "x": 1502283360000, "y": 2097748.6799999983 }, - { "x": 1502283420000, "y": 1386087.6600000001 }, - { "x": 1502283480000, "y": 1509311.1599999992 }, - { "x": 1502283540000, "y": 1165877.2800000003 }, - { "x": 1502283600000, "y": 1183434.8 }, - { "x": 1502283660000, "y": 1425065.5000000007 }, - { "x": 1502283720000, "y": 1820377.1200000006 }, - { "x": 1502283780000, "y": 1996905.9000000004 }, - { "x": 1502283840000, "y": 2199604.54 }, - { "x": 1502283900000, "y": 1443694.2499999998 }, - { "x": 1502283960000, "y": 1261225.6 }, - { "x": 1502284020000, "y": 1588579.5600000003 }, - { "x": 1502284080000, "y": 2073728.899999998 }, - { "x": 1502284140000, "y": 1330845.0100000002 }, - { "x": 1502284200000, "y": 1160146.2399999998 }, - { "x": 1502284260000, "y": 1623945.5 }, - { "x": 1502284320000, "y": 1390707.1400000001 }, - { "x": 1502284380000, "y": 2067623.4500000002 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 2547299.079999993 }, - { "x": 1502284560000, "y": 4586742.89999998 }, - { "x": 1502284620000, "y": 0 } - ] - }, - "tpmBuckets": [ - { - "key": "2xx", - "dataPoints": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 0 }, - { "x": 1502282940000, "y": 33 }, - { "x": 1502283000000, "y": 42 }, - { "x": 1502283060000, "y": 44 }, - { "x": 1502283120000, "y": 42 }, - { "x": 1502283180000, "y": 47 }, - { "x": 1502283240000, "y": 42 }, - { "x": 1502283300000, "y": 35 }, - { "x": 1502283360000, "y": 44 }, - { "x": 1502283420000, "y": 39 }, - { "x": 1502283480000, "y": 34 }, - { "x": 1502283540000, "y": 38 }, - { "x": 1502283600000, "y": 37 }, - { "x": 1502283660000, "y": 41 }, - { "x": 1502283720000, "y": 37 }, - { "x": 1502283780000, "y": 37 }, - { "x": 1502283840000, "y": 52 }, - { "x": 1502283900000, "y": 38 }, - { "x": 1502283960000, "y": 43 }, - { "x": 1502284020000, "y": 38 }, - { "x": 1502284080000, "y": 41 }, - { "x": 1502284140000, "y": 40 }, - { "x": 1502284200000, "y": 42 }, - { "x": 1502284260000, "y": 40 }, - { "x": 1502284320000, "y": 49 }, - { "x": 1502284380000, "y": 51 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 0 }, - { "x": 1502284560000, "y": 56 }, - { "x": 1502284620000, "y": 0 } - ] - }, - { - "key": "3xx", - "dataPoints": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 0 }, - { "x": 1502282940000, "y": 0 }, - { "x": 1502283000000, "y": 0 }, - { "x": 1502283060000, "y": 0 }, - { "x": 1502283120000, "y": 0 }, - { "x": 1502283180000, "y": 0 }, - { "x": 1502283240000, "y": 0 }, - { "x": 1502283300000, "y": 0 }, - { "x": 1502283360000, "y": 0 }, - { "x": 1502283420000, "y": 0 }, - { "x": 1502283480000, "y": 0 }, - { "x": 1502283540000, "y": 0 }, - { "x": 1502283600000, "y": 0 }, - { "x": 1502283660000, "y": 0 }, - { "x": 1502283720000, "y": 0 }, - { "x": 1502283780000, "y": 0 }, - { "x": 1502283840000, "y": 0 }, - { "x": 1502283900000, "y": 0 }, - { "x": 1502283960000, "y": 0 }, - { "x": 1502284020000, "y": 0 }, - { "x": 1502284080000, "y": 0 }, - { "x": 1502284140000, "y": 0 }, - { "x": 1502284200000, "y": 0 }, - { "x": 1502284260000, "y": 0 }, - { "x": 1502284320000, "y": 0 }, - { "x": 1502284380000, "y": 0 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 0 }, - { "x": 1502284560000, "y": 0 }, - { "x": 1502284620000, "y": 0 } - ] - }, - { - "key": "4xx", - "dataPoints": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 0 }, - { "x": 1502282940000, "y": 1 }, - { "x": 1502283000000, "y": 1 }, - { "x": 1502283060000, "y": 1 }, - { "x": 1502283120000, "y": 3 }, - { "x": 1502283180000, "y": 1 }, - { "x": 1502283240000, "y": 1 }, - { "x": 1502283300000, "y": 1 }, - { "x": 1502283360000, "y": 1 }, - { "x": 1502283420000, "y": 1 }, - { "x": 1502283480000, "y": 3 }, - { "x": 1502283540000, "y": 1 }, - { "x": 1502283600000, "y": 1 }, - { "x": 1502283660000, "y": 1 }, - { "x": 1502283720000, "y": 1 }, - { "x": 1502283780000, "y": 1 }, - { "x": 1502283840000, "y": 2 }, - { "x": 1502283900000, "y": 2 }, - { "x": 1502283960000, "y": 1 }, - { "x": 1502284020000, "y": 1 }, - { "x": 1502284080000, "y": 1 }, - { "x": 1502284140000, "y": 1 }, - { "x": 1502284200000, "y": 2 }, - { "x": 1502284260000, "y": 2 }, - { "x": 1502284320000, "y": 2 }, - { "x": 1502284380000, "y": 3 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 0 }, - { "x": 1502284560000, "y": 2 }, - { "x": 1502284620000, "y": 0 } - ] - }, - { - "key": "5xx", - "dataPoints": [ - { "x": 1502282820000, "y": 0 }, - { "x": 1502282880000, "y": 0 }, - { "x": 1502282940000, "y": 5 }, - { "x": 1502283000000, "y": 6 }, - { "x": 1502283060000, "y": 5 }, - { "x": 1502283120000, "y": 4 }, - { "x": 1502283180000, "y": 4 }, - { "x": 1502283240000, "y": 4 }, - { "x": 1502283300000, "y": 5 }, - { "x": 1502283360000, "y": 4 }, - { "x": 1502283420000, "y": 4 }, - { "x": 1502283480000, "y": 6 }, - { "x": 1502283540000, "y": 6 }, - { "x": 1502283600000, "y": 5 }, - { "x": 1502283660000, "y": 6 }, - { "x": 1502283720000, "y": 7 }, - { "x": 1502283780000, "y": 6 }, - { "x": 1502283840000, "y": 8 }, - { "x": 1502283900000, "y": 6 }, - { "x": 1502283960000, "y": 9 }, - { "x": 1502284020000, "y": 6 }, - { "x": 1502284080000, "y": 4 }, - { "x": 1502284140000, "y": 3 }, - { "x": 1502284200000, "y": 6 }, - { "x": 1502284260000, "y": 9 }, - { "x": 1502284320000, "y": 4 }, - { "x": 1502284380000, "y": 8 }, - { "x": 1502284440000, "y": 0 }, - { "x": 1502284500000, "y": 0 }, - { "x": 1502284560000, "y": 8 }, - { "x": 1502284620000, "y": 0 } - ] - } - ], - "overallAvgDuration": 467582.45401459857, - "noHits": false -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js deleted file mode 100644 index 7183c4851e993..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js +++ /dev/null @@ -1,122 +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 { isEmpty } from 'lodash'; -import { Hint } from 'react-vis'; -import styled from 'styled-components'; -import PropTypes from 'prop-types'; -import { - unit, - units, - px, - borderRadius, - fontSize, - fontSizes, -} from '../../../../style/variables'; -import { Legend } from '../Legend'; -import { asAbsoluteDateTime } from '../../../../../common/utils/formatters'; - -const TooltipElm = styled.div` - margin: 0 ${px(unit)}; - transform: translateY(-50%); - border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - background: ${({ theme }) => theme.eui.euiColorEmptyShade}; - border-radius: ${borderRadius}; - font-size: ${fontSize}; - color: ${({ theme }) => theme.eui.euiColorFullShade}; -`; - -const Header = styled.div` - background: ${({ theme }) => theme.eui.euiColorLightestShade}; - border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - border-radius: ${borderRadius} ${borderRadius} 0 0; - padding: ${px(units.half)}; - color: ${({ theme }) => theme.eui.euiColorMediumShade}; -`; - -const Content = styled.div` - margin: ${px(units.half)}; - margin-right: ${px(unit)}; - font-size: ${fontSizes.small}; -`; - -const Footer = styled.div` - color: ${({ theme }) => theme.eui.euiColorMediumShade}; - margin: ${px(units.half)}; - font-size: ${fontSizes.small}; -`; - -const LegendContainer = styled.div` - display: flex; - align-items: center; - margin-bottom: ${px(units.quarter)}; - justify-content: space-between; -`; - -const LegendGray = styled(Legend)` - color: ${({ theme }) => theme.eui.euiColorMediumShade}; - padding-bottom: 0; - padding-right: ${px(units.half)}; -`; - -const Value = styled.div` - color: ${({ theme }) => theme.eui.euiColorDarkShade}; - font-size: ${fontSize}; -`; - -export default function Tooltip({ - header, - footer, - tooltipPoints, - x, - y, - ...props -}) { - if (isEmpty(tooltipPoints)) { - return null; - } - - // Only show legend labels if there is more than 1 data set - const showLegends = tooltipPoints.length > 1; - - return ( - <Hint {...props} value={{ x, y }}> - <TooltipElm> - <Header>{header || asAbsoluteDateTime(x, 'seconds')}</Header> - - <Content> - {showLegends ? ( - tooltipPoints.map((point, i) => ( - <LegendContainer key={i}> - <LegendGray - fontSize={fontSize.tiny} - radius={units.half} - color={point.color} - text={point.text} - /> - - <Value>{point.value}</Value> - </LegendContainer> - )) - ) : ( - <Value>{tooltipPoints[0].value}</Value> - )} - </Content> - <Footer>{footer}</Footer> - </TooltipElm> - </Hint> - ); -} - -Tooltip.propTypes = { - header: PropTypes.string, - tooltipPoints: PropTypes.array.isRequired, - x: PropTypes.number, - y: PropTypes.number, -}; - -Tooltip.defaultProps = {}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/get_empty_series.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/get_empty_series.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/get_timezone_offset_in_ms.test.ts similarity index 95% rename from x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/get_timezone_offset_in_ms.test.ts index 935895022931c..f45e207c32c8f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_timezone_offset_in_ms.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs'; +import { getTimezoneOffsetInMs } from './get_timezone_offset_in_ms'; import moment from 'moment-timezone'; // FAILING: https://github.com/elastic/kibana/issues/50005 diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/get_timezone_offset_in_ms.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/get_timezone_offset_in_ms.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts index d0301880ef52a..ca328473db8cc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import d3 from 'd3'; -import { getTimezoneOffsetInMs } from '../CustomPlot/getTimezoneOffsetInMs'; +import { getTimezoneOffsetInMs } from './get_timezone_offset_in_ms'; interface Params { domain: [number, number]; diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx similarity index 54% rename from x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 2f63a77132be9..9a561571df5a7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -6,54 +6,18 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; import { - asPercent, asDecimal, + asDuration, asInteger, - asDynamicBytes, + asPercent, getFixedByteFormatter, - asDuration, } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; -// @ts-expect-error -import CustomPlot from '../CustomPlot'; -import { Coordinate } from '../../../../../typings/timeseries'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync'; import { Maybe } from '../../../../../typings/common'; - -interface Props { - start: Maybe<number | string>; - end: Maybe<number | string>; - chart: GenericMetricsChart; -} - -export function MetricsChart({ chart }: Props) { - const formatYValue = getYTickFormatter(chart); - const formatTooltip = getTooltipFormatter(chart); - - const transformedSeries = chart.series.map((series) => ({ - ...series, - legendValue: formatYValue(series.overallValue), - })); - - const syncedChartProps = useChartsSync(); - - return ( - <React.Fragment> - <EuiTitle size="xs"> - <span>{chart.title}</span> - </EuiTitle> - <CustomPlot - {...syncedChartProps} - series={transformedSeries} - tickFormatY={formatYValue} - formatTooltipValue={formatTooltip} - yMax={chart.yUnit === 'percent' ? 1 : 'max'} - /> - </React.Fragment> - ); -} +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { TimeseriesChart } from '../timeseries_chart'; function getYTickFormatter(chart: GenericMetricsChart) { switch (chart.yUnit) { @@ -82,24 +46,25 @@ function getYTickFormatter(chart: GenericMetricsChart) { } } -function getTooltipFormatter({ yUnit }: GenericMetricsChart) { - switch (yUnit) { - case 'bytes': { - return (c: Coordinate) => asDynamicBytes(c.y); - } - case 'percent': { - return (c: Coordinate) => asPercent(c.y || 0, 1); - } - case 'time': { - return (c: Coordinate) => asDuration(c.y); - } - case 'integer': { - return (c: Coordinate) => - isValidCoordinateValue(c.y) ? asInteger(c.y) : c.y; - } - default: { - return (c: Coordinate) => - isValidCoordinateValue(c.y) ? asDecimal(c.y) : c.y; - } - } +interface Props { + start: Maybe<number | string>; + end: Maybe<number | string>; + chart: GenericMetricsChart; + fetchStatus: FETCH_STATUS; +} + +export function MetricsChart({ chart, fetchStatus }: Props) { + return ( + <> + <EuiTitle size="xs"> + <span>{chart.title}</span> + </EuiTitle> + <TimeseriesChart + fetchStatus={fetchStatus} + id={chart.key} + timeseries={chart.series} + yLabelFormat={getYTickFormatter(chart) as (y: number) => string} + /> + </> + ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 18b914afea995..6f1f4e01c4d1f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { ScaleType, Chart, Settings, AreaSeries } from '@elastic/charts'; +import { + ScaleType, + Chart, + Settings, + AreaSeries, + CurveType, +} from '@elastic/charts'; import { EuiIcon } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; @@ -15,16 +21,15 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; interface Props { color: string; - series: Array<{ x: number; y: number | null }>; + series?: Array<{ x: number; y: number | null }>; + width: string; } export function SparkPlot(props: Props) { - const { series, color } = props; + const { series, color, width } = props; const chartTheme = useChartTheme(); - const isEmpty = series.every((point) => point.y === null); - - if (isEmpty) { + if (!series || series.every((point) => point.y === null)) { return ( <EuiFlexGroup gutterSize="s" alignItems="center"> <EuiFlexItem grow={false}> @@ -40,7 +45,7 @@ export function SparkPlot(props: Props) { } return ( - <Chart size={{ height: px(24), width: px(64) }}> + <Chart size={{ height: px(24), width }}> <Settings theme={{ ...chartTheme, @@ -60,6 +65,7 @@ export function SparkPlot(props: Props) { yAccessors={['y']} data={series} color={color} + curve={CurveType.CURVE_MONOTONE_X} /> </Chart> ); diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx index e2bb42fddb33b..3819ed30d104a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; - +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; +import { px, unit } from '../../../../../style/variables'; import { useTheme } from '../../../../../hooks/useTheme'; -import { getEmptySeries } from '../../CustomPlot/getEmptySeries'; import { SparkPlot } from '../'; type Color = @@ -25,17 +23,15 @@ type Color = | 'euiColorVis9'; export function SparkPlotWithValueLabel({ - start, - end, color, series, valueLabel, + compact, }: { - start: number; - end: number; color: Color; series?: Array<{ x: number; y: number | null }>; valueLabel: React.ReactNode; + compact?: boolean; }) { const theme = useTheme(); @@ -45,7 +41,8 @@ export function SparkPlotWithValueLabel({ <EuiFlexGroup gutterSize="m"> <EuiFlexItem grow={false}> <SparkPlot - series={series ?? getEmptySeries(start, end)[0].data} + series={series} + width={compact ? px(unit * 3) : px(unit * 4)} color={colorValue} /> </EuiFlexItem> diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx similarity index 84% rename from x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index b40df89a22c33..918e940651dee 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -5,8 +5,10 @@ */ import { + AreaSeries, Axis, Chart, + CurveType, LegendItemListener, LineSeries, niceTimeFormatter, @@ -19,14 +21,14 @@ import { import moment from 'moment'; import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { TimeSeries } from '../../../../../typings/timeseries'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useChartsSync } from '../../../../hooks/use_charts_sync'; -import { unit } from '../../../../style/variables'; -import { Annotations } from '../annotations'; -import { ChartContainer } from '../chart_container'; -import { onBrushEnd } from '../helper/helper'; +import { TimeSeries } from '../../../../typings/timeseries'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useChartsSync } from '../../../hooks/use_charts_sync'; +import { unit } from '../../../style/variables'; +import { Annotations } from './annotations'; +import { ChartContainer } from './chart_container'; +import { onBrushEnd } from './helper/helper'; interface Props { id: string; @@ -45,7 +47,7 @@ interface Props { showAnnotations?: boolean; } -export function LineChart({ +export function TimeseriesChart({ id, height = unit * 16, fetchStatus, @@ -127,8 +129,10 @@ export function LineChart({ {showAnnotations && <Annotations />} {timeseries.map((serie) => { + const Series = serie.type === 'area' ? AreaSeries : LineSeries; + return ( - <LineSeries + <Series key={serie.title} id={serie.title} xScaleType={ScaleType.Time} @@ -137,6 +141,7 @@ export function LineChart({ yAccessors={['y']} data={isEmpty ? [] : serie.data} color={serie.color} + curve={CurveType.CURVE_MONOTONE_X} /> ); })} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 2a5948d0ebf0b..41212aa7b982c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -26,10 +26,10 @@ import { ChartsSyncContextProvider } from '../../../../context/charts_sync_conte import { LicenseContext } from '../../../../context/LicenseContext'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { ITransactionChartData } from '../../../../selectors/chartSelectors'; +import { ITransactionChartData } from '../../../../selectors/chart_selectors'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { TransactionBreakdown } from '../../TransactionBreakdown'; -import { LineChart } from '../line_chart'; +import { TimeseriesChart } from '../timeseries_chart'; import { TransactionErrorRateChart } from '../transaction_error_rate_chart/'; import { getResponseTimeTickFormatter } from './helper'; import { MLHeader } from './ml_header'; @@ -81,7 +81,7 @@ export function TransactionCharts({ )} </LicenseContext.Consumer> </EuiFlexGroup> - <LineChart + <TimeseriesChart fetchStatus={fetchStatus} id="transactionDuration" timeseries={responseTimeSeries || []} @@ -100,7 +100,7 @@ export function TransactionCharts({ <EuiTitle size="xs"> <span>{tpmLabel(transactionType)}</span> </EuiTitle> - <LineChart + <TimeseriesChart fetchStatus={fetchStatus} id="requestPerMinutes" timeseries={tpmSeries || []} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/use_formatter.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_charts/use_formatter.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/use_formatter.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts rename to x-pack/plugins/apm/public/components/shared/charts/transaction_charts/use_formatter.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index dd9a1e2ec2efe..b9028ff2e9e8c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -13,7 +13,7 @@ import { useFetcher } from '../../../../hooks/useFetcher'; import { useTheme } from '../../../../hooks/useTheme'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -import { LineChart } from '../line_chart'; +import { TimeseriesChart } from '../timeseries_chart'; function yLabelFormat(y?: number | null) { return asPercent(y || 0, 1); @@ -73,7 +73,7 @@ export function TransactionErrorRateChart({ })} </h2> </EuiTitle> - <LineChart + <TimeseriesChart id="errorRate" height={height} showAnnotations={showAnnotations} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx similarity index 74% rename from x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx rename to x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx index 912490d866e88..e8d62cd8bd85b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx +++ b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx @@ -5,10 +5,10 @@ */ import React, { ReactNode } from 'react'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../ErrorStatePrompt'; -export function FetchWrapper({ +export function TableFetchWrapper({ status, children, }: { diff --git a/x-pack/plugins/apm/public/context/charts_sync_context.tsx b/x-pack/plugins/apm/public/context/charts_sync_context.tsx index 282097fed2460..d983a857a26ec 100644 --- a/x-pack/plugins/apm/public/context/charts_sync_context.tsx +++ b/x-pack/plugins/apm/public/context/charts_sync_context.tsx @@ -4,91 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, useMemo, useState } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; -import { useFetcher } from '../hooks/useFetcher'; -import { useUrlParams } from '../hooks/useUrlParams'; - -export const LegacyChartsSyncContext = React.createContext<{ - hoverX: number | null; - onHover: (hoverX: number) => void; - onMouseLeave: () => void; - onSelectionEnd: (range: { start: number; end: number }) => void; -} | null>(null); - -export function LegacyChartsSyncContextProvider({ - children, -}: { - children: ReactNode; -}) { - const history = useHistory(); - const [time, setTime] = useState<number | null>(null); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { urlParams, uiFilters } = useUrlParams(); - - const { start, end } = urlParams; - const { environment } = uiFilters; - - const { data = { annotations: [] } } = useFetcher( - (callApmApi) => { - if (start && end && serviceName) { - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', - params: { - path: { - serviceName, - }, - query: { - start, - end, - environment, - }, - }, - }); - } - }, - [start, end, environment, serviceName] - ); - - const value = useMemo(() => { - const hoverXHandlers = { - onHover: (hoverX: number) => { - setTime(hoverX); - }, - onMouseLeave: () => { - setTime(null); - }, - onSelectionEnd: (range: { start: number; end: number }) => { - setTime(null); - - const currentSearch = toQuery(history.location.search); - const nextSearch = { - rangeFrom: new Date(range.start).toISOString(), - rangeTo: new Date(range.end).toISOString(), - }; - - history.push({ - ...history.location, - search: fromQuery({ - ...currentSearch, - ...nextSearch, - }), - }); - }, - hoverX: time, - annotations: data.annotations, - }; - - return { ...hoverXHandlers }; - }, [history, time, data.annotations]); - - return <LegacyChartsSyncContext.Provider value={value} children={children} />; -} - -export const ChartsSyncContext = React.createContext<{ +import React, { + createContext, + Dispatch, + ReactNode, + SetStateAction, + useState, +} from 'react'; + +export const ChartsSyncContext = createContext<{ event: any; - setEvent: Function; + setEvent: Dispatch<SetStateAction<{}>>; } | null>(null); export function ChartsSyncContextProvider({ diff --git a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts index 78ea30f466cfa..c790ac57edc3b 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts @@ -6,7 +6,7 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { getTransactionCharts } from '../selectors/chartSelectors'; +import { getTransactionCharts } from '../selectors/chart_selectors'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 36b5a7c00d4be..9980569ee54dd 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -9,13 +9,16 @@ import { useHistory, useParams } from 'react-router-dom'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { useUiFilters } from '../context/UrlParamsContext'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { maybe } from '../../common/utils/maybe'; +import { APIReturnType } from '../services/rest/createCallApmApi'; + +type APIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; const INITIAL_DATA = { - buckets: [] as TransactionDistributionAPIResponse['buckets'], + buckets: [] as APIResponse['buckets'], noHits: true, bucketSize: 0, }; diff --git a/x-pack/plugins/apm/public/hooks/use_charts_sync.tsx b/x-pack/plugins/apm/public/hooks/use_charts_sync.tsx index 52c7e4c1e3a31..cde5c84a6097b 100644 --- a/x-pack/plugins/apm/public/hooks/use_charts_sync.tsx +++ b/x-pack/plugins/apm/public/hooks/use_charts_sync.tsx @@ -5,10 +5,7 @@ */ import { useContext } from 'react'; -import { - ChartsSyncContext, - LegacyChartsSyncContext, -} from '../context/charts_sync_context'; +import { ChartsSyncContext } from '../context/charts_sync_context'; export function useChartsSync() { const context = useContext(ChartsSyncContext); @@ -19,13 +16,3 @@ export function useChartsSync() { return context; } - -export function useLegacyChartsSync() { - const context = useContext(LegacyChartsSyncContext); - - if (!context) { - throw new Error('Missing ChartsSync context provider'); - } - - return context; -} diff --git a/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts similarity index 97% rename from x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts rename to x-pack/plugins/apm/public/selectors/chart_selectors.test.ts index 901e6052bbf06..4269ec0e6c0f3 100644 --- a/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts +++ b/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts @@ -9,16 +9,16 @@ import { getAnomalyScoreSeries, getResponseTimeSeries, getTpmSeries, -} from '../chartSelectors'; +} from './chart_selectors'; import { successColor, warningColor, errorColor, -} from '../../utils/httpStatusCodeToColor'; +} from '../utils/httpStatusCodeToColor'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ApmTimeSeriesResponse } from '../../../server/lib/transactions/charts/get_timeseries_data/transform'; +import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform'; -describe('chartSelectors', () => { +describe('chart selectors', () => { describe('getAnomalyScoreSeries', () => { it('should return anomalyScoreSeries', () => { const data = [{ x0: 0, x: 10 }]; diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.ts similarity index 98% rename from x-pack/plugins/apm/public/selectors/chartSelectors.ts rename to x-pack/plugins/apm/public/selectors/chart_selectors.ts index 450f02f70c6a4..8330df07c21eb 100644 --- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/selectors/chart_selectors.ts @@ -18,7 +18,7 @@ import { TimeSeries, } from '../../typings/timeseries'; import { IUrlParams } from '../context/UrlParamsContext/types'; -import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; +import { getEmptySeries } from '../components/shared/charts/helper/get_empty_series'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; import { asDecimal, asDuration, tpmUnit } from '../../common/utils/formatters'; diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index 0adfb99e7164e..00d7e8e1dd5e4 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -115,6 +115,12 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) _Note: Run the following commands from `kibana/`._ +### Typescript + +``` +yarn tsc --noEmit --project x-pack/tsconfig.json --skipLibCheck +``` + ### Prettier ``` diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts index c1cb903a0bb3e..0df1cbf0e0eef 100644 --- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts +++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts @@ -23,7 +23,6 @@ import { TRANSACTION_RESULT, PROCESSOR_EVENT, } from '../../common/elasticsearch_fieldnames'; -import { stampLogger } from '../shared/stamp-logger'; import { createOrUpdateIndex } from '../shared/create-or-update-index'; import { parseIndexUrl } from '../shared/parse_index_url'; import { ESClient, getEsClient } from '../shared/get_es_client'; @@ -49,8 +48,6 @@ import { ESClient, getEsClient } from '../shared/get_es_client'; // default ones. // - exclude: comma-separated list of fields that should be not be aggregated on. -stampLogger(); - export async function aggregateLatencyMetrics() { const interval = parseInt(String(argv.interval), 10) || 1; const concurrency = parseInt(String(argv.concurrency), 10) || 3; diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts index 723ff03dc4995..4739a5b621972 100644 --- a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts +++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts @@ -9,11 +9,8 @@ import { execSync } from 'child_process'; import moment from 'moment'; import path from 'path'; import fs from 'fs'; -import { stampLogger } from '../shared/stamp-logger'; async function run() { - stampLogger(); - const archiveName = 'apm_8.0.0'; // include important APM data and ML data diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index ca47540b04d82..8c64c37d9b7f7 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -15,7 +15,6 @@ import { merge, chunk, flatten, omit } from 'lodash'; import { Client } from '@elastic/elasticsearch'; import { argv } from 'yargs'; import { Logger } from 'kibana/server'; -import { stampLogger } from '../shared/stamp-logger'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; import { downloadTelemetryTemplate } from '../shared/download-telemetry-template'; @@ -25,8 +24,6 @@ import { readKibanaConfig } from '../shared/read-kibana-config'; import { getHttpAuth } from '../shared/get-http-auth'; import { createOrUpdateIndex } from '../shared/create-or-update-index'; -stampLogger(); - async function uploadData() { const githubToken = process.env.GITHUB_TOKEN; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index a10762622b2c6..449aa88752f21 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -10,7 +10,6 @@ import { snakeCase } from 'lodash'; import Boom from '@hapi/boom'; import { ProcessorEvent } from '../../../common/processor_event'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { TRANSACTION_DURATION, @@ -19,9 +18,6 @@ import { import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; -export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< - typeof createAnomalyDetectionJobs ->; export async function createAnomalyDetectionJobs( setup: Setup, environments: string[], diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts new file mode 100644 index 0000000000000..ba739310bc342 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { + formatTopSignificantTerms, + TopSigTerm, +} from '../get_correlations_for_slow_transactions/format_top_significant_terms'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getOutcomeAggregation, + getTransactionErrorRateTimeSeries, +} from '../../helpers/transaction_error_rate'; + +export async function getCorrelationsForFailedTransactions({ + serviceName, + transactionType, + transactionName, + fieldNames, + setup, +}: { + serviceName: string | undefined; + transactionType: string | undefined; + transactionName: string | undefined; + fieldNames: string[]; + setup: Setup & SetupTimeRange; +}) { + const { start, end, esFilter, apmEventClient } = setup; + + const backgroundFilters: ESFilter[] = [ + ...esFilter, + { range: rangeFilter(start, end) }, + ]; + + if (serviceName) { + backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } }); + } + + if (transactionType) { + backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); + } + + if (transactionName) { + backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); + } + + const params = { + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { + bool: { + // foreground filters + filter: [ + ...backgroundFilters, + { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + ], + }, + }, + aggs: fieldNames.reduce((acc, fieldName) => { + return { + ...acc, + [fieldName]: { + significant_terms: { + size: 10, + field: fieldName, + background_filter: { bool: { filter: backgroundFilters } }, + }, + }, + }; + }, {} as Record<string, { significant_terms: AggregationOptionsByType['significant_terms'] }>), + }, + }; + + const response = await apmEventClient.search(params); + const topSigTerms = formatTopSignificantTerms(response.aggregations); + return getChartsForTopSigTerms({ setup, backgroundFilters, topSigTerms }); +} + +export async function getChartsForTopSigTerms({ + setup, + backgroundFilters, + topSigTerms, +}: { + setup: Setup & SetupTimeRange; + backgroundFilters: ESFilter[]; + topSigTerms: TopSigTerm[]; +}) { + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 30 }); + + if (isEmpty(topSigTerms)) { + return {}; + } + + const timeseriesAgg = { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + // TODO: add support for metrics + outcomes: getOutcomeAggregation({ searchAggregatedTransactions: false }), + }, + }; + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { timeseries: timeseriesAgg }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { timeseries: typeof timeseriesAgg }; + } + > + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: backgroundFilters } }, + aggs: { + // overall aggs + timeseries: timeseriesAgg, + + // per term aggs + ...perTermAggs, + }, + }, + }; + + const response = await apmEventClient.search(params); + type Agg = NonNullable<typeof response.aggregations>; + + if (!response.aggregations) { + return {}; + } + + return { + overall: { + timeseries: getTransactionErrorRateTimeSeries( + response.aggregations.timeseries.buckets + ), + }, + significantTerms: topSigTerms.map((topSig, index) => { + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg; + + return { + ...topSig, + timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets), + }; + }), + }; +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/format_top_significant_terms.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/format_top_significant_terms.ts new file mode 100644 index 0000000000000..f168b49fb18fd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/format_top_significant_terms.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { orderBy } from 'lodash'; +import { + AggregationOptionsByType, + AggregationResultOf, +} from '../../../../../../typings/elasticsearch/aggregations'; + +export interface TopSigTerm { + bgCount: number; + fgCount: number; + fieldName: string; + fieldValue: string | number; + score: number; +} + +type SigTermAggs = AggregationResultOf< + { significant_terms: AggregationOptionsByType['significant_terms'] }, + {} +>; + +export function formatTopSignificantTerms( + aggregations?: Record<string, SigTermAggs> +) { + const significantTerms = Object.entries(aggregations ?? []).flatMap( + ([fieldName, agg]) => { + return agg.buckets.map((bucket) => ({ + fieldName, + fieldValue: bucket.key, + bgCount: bucket.bg_count, + fgCount: bucket.doc_count, + score: bucket.score, + })); + } + ); + + // get top 10 terms ordered by score + const topSigTerms = orderBy(significantTerms, 'score', 'desc').slice(0, 10); + return topSigTerms; +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_charts_for_top_sig_terms.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_charts_for_top_sig_terms.ts new file mode 100644 index 0000000000000..cbefd5e2133e5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_charts_for_top_sig_terms.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { TopSigTerm } from './format_top_significant_terms'; +import { getMaxLatency } from './get_max_latency'; + +export async function getChartsForTopSigTerms({ + setup, + backgroundFilters, + topSigTerms, +}: { + setup: Setup & SetupTimeRange; + backgroundFilters: ESFilter[]; + topSigTerms: TopSigTerm[]; +}) { + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 30 }); + + if (isEmpty(topSigTerms)) { + return {}; + } + + const maxLatency = await getMaxLatency({ + setup, + backgroundFilters, + topSigTerms, + }); + + if (!maxLatency) { + return {}; + } + + const intervalBuckets = 20; + const distributionInterval = roundtoTenth(maxLatency / intervalBuckets); + + const distributionAgg = { + // filter out outliers not included in the significant term docs + filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } }, + aggs: { + dist_filtered_by_latency: { + histogram: { + // TODO: add support for metrics + field: TRANSACTION_DURATION, + interval: distributionInterval, + min_doc_count: 0, + extended_bounds: { + min: 0, + max: maxLatency, + }, + }, + }, + }, + }; + + const timeseriesAgg = { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + average: { + avg: { + // TODO: add support for metrics + field: TRANSACTION_DURATION, + }, + }, + }, + }; + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { + distribution: distributionAgg, + timeseries: timeseriesAgg, + }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { + distribution: typeof distributionAgg; + timeseries: typeof timeseriesAgg; + }; + } + > + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: backgroundFilters } }, + aggs: { + // overall aggs + distribution: distributionAgg, + timeseries: timeseriesAgg, + + // per term aggs + ...perTermAggs, + }, + }, + }; + + const response = await apmEventClient.search(params); + type Agg = NonNullable<typeof response.aggregations>; + + if (!response.aggregations) { + return; + } + + function formatTimeseries(timeseries: Agg['timeseries']) { + return timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.average.value, + })); + } + + function formatDistribution(distribution: Agg['distribution']) { + const total = distribution.doc_count; + return distribution.dist_filtered_by_latency.buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })); + } + + return { + distributionInterval, + overall: { + timeseries: formatTimeseries(response.aggregations.timeseries), + distribution: formatDistribution(response.aggregations.distribution), + }, + significantTerms: topSigTerms.map((topSig, index) => { + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg; + + return { + ...topSig, + timeseries: formatTimeseries(agg.timeseries), + distribution: formatDistribution(agg.distribution), + }; + }), + }; +} + +function roundtoTenth(v: number) { + return Math.pow(10, Math.round(Math.log10(v))); +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts rename to x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts new file mode 100644 index 0000000000000..3f86d2900e85b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { TopSigTerm } from './format_top_significant_terms'; + +export async function getMaxLatency({ + setup, + backgroundFilters, + topSigTerms, +}: { + setup: Setup & SetupTimeRange; + backgroundFilters: ESFilter[]; + topSigTerms: TopSigTerm[]; +}) { + const { apmEventClient } = setup; + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { + bool: { + filter: backgroundFilters, + + // only include docs containing the significant terms + should: topSigTerms.map((term) => ({ + term: { [term.fieldName]: term.fieldValue }, + })), + minimum_should_match: 1, + }, + }, + aggs: { + // TODO: add support for metrics + // max_latency: { max: { field: TRANSACTION_DURATION } }, + max_latency: { + percentiles: { field: TRANSACTION_DURATION, percents: [99] }, + }, + }, + }, + }; + + const response = await apmEventClient.search(params); + // return response.aggregations?.max_latency.value; + return Object.values(response.aggregations?.max_latency.values ?? {})[0]; +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts rename to x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts index 76e595c928cf2..b8a5ab93591a4 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { SERVICE_NAME, TRANSACTION_DURATION, @@ -12,15 +14,10 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { asDuration } from '../../../../common/utils/formatters'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getDurationForPercentile } from './get_duration_for_percentile'; -import { - formatAggregationResponse, - getSignificantTermsAgg, -} from './get_significant_terms_agg'; -import { SignificantTermsScoring } from './scoring_rt'; +import { formatTopSignificantTerms } from './format_top_significant_terms'; +import { getChartsForTopSigTerms } from './get_charts_for_top_sig_terms'; export async function getCorrelationsForSlowTransactions({ serviceName, @@ -28,13 +25,11 @@ export async function getCorrelationsForSlowTransactions({ transactionName, durationPercentile, fieldNames, - scoring, setup, }: { serviceName: string | undefined; transactionType: string | undefined; transactionName: string | undefined; - scoring: SignificantTermsScoring; durationPercentile: number; fieldNames: string[]; setup: Setup & SetupTimeRange; @@ -79,16 +74,22 @@ export async function getCorrelationsForSlowTransactions({ ], }, }, - aggs: getSignificantTermsAgg({ fieldNames, backgroundFilters, scoring }), + aggs: fieldNames.reduce((acc, fieldName) => { + return { + ...acc, + [fieldName]: { + significant_terms: { + size: 10, + field: fieldName, + background_filter: { bool: { filter: backgroundFilters } }, + }, + }, + }; + }, {} as Record<string, { significant_terms: AggregationOptionsByType['significant_terms'] }>), }, }; const response = await apmEventClient.search(params); - - return { - message: `Showing significant fields for transactions slower than ${durationPercentile}th percentile (${asDuration( - durationForPercentile - )})`, - response: formatAggregationResponse(response.aggregations), - }; + const topSigTerms = formatTopSignificantTerms(response.aggregations); + return getChartsForTopSigTerms({ setup, backgroundFilters, topSigTerms }); } diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index 7866c99353451..8c63d097fe56e 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { BUCKET_TARGET_COUNT } from '../../transactions/constants'; import { getBuckets } from './get_buckets'; @@ -13,10 +12,6 @@ function getBucketSize({ start, end }: SetupTimeRange) { return Math.floor((end - start) / BUCKET_TARGET_COUNT); } -export type ErrorDistributionAPIResponse = PromiseReturnType< - typeof getErrorDistribution ->; - export async function getErrorDistribution({ serviceName, groupId, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 37be72beedeb1..965cc28952b7a 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_GROUP_ID, SERVICE_NAME, @@ -15,8 +14,6 @@ import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; -export type ErrorGroupAPIResponse = PromiseReturnType<typeof getErrorGroup>; - // TODO: rename from "getErrorGroup" to "getErrorGroupSample" (since a single error is returned, not an errorGroup) export async function getErrorGroup({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 97c03924538c8..3a3cf02297701 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -5,7 +5,6 @@ */ import { SortOptions } from '../../../../../typings/elasticsearch/aggregations'; -import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -19,10 +18,6 @@ import { mergeProjection } from '../../projections/util/merge_projection'; import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -export type ErrorGroupListAPIResponse = PromiseReturnType< - typeof getErrorGroups ->; - export async function getErrorGroups({ serviceName, sortField, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 5b78d97d5b681..2a891bc6f8990 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -8,11 +8,15 @@ import moment from 'moment'; // @ts-expect-error import { calculateAuto } from './calculate_auto'; -export function getBucketSize( - start: number, - end: number, - numBuckets: number = 100 -) { +export function getBucketSize({ + start, + end, + numBuckets = 100, +}: { + start: number; + end: number; + numBuckets?: number; +}) { const duration = moment.duration(end - start, 'ms'); const bucketSize = Math.max( calculateAuto.near(numBuckets, duration).asSeconds(), diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index ea018868f9517..7ea8dc35b41d0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -11,7 +11,7 @@ export function getMetricsDateHistogramParams( end: number, metricsInterval: number ) { - const { bucketSize } = getBucketSize(start, end); + const { bucketSize } = getBucketSize({ start, end }); return { field: '@timestamp', diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 03a44e77ba2d3..536be56e152a3 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -20,6 +20,8 @@ export function getOutcomeAggregation({ return { terms: { field: EVENT_OUTCOME }, aggs: { + // simply using the doc count to get the number of requests is not possible for transaction metrics (histograms) + // to work around this we get the number of transactions by counting the number of latency values count: { value_count: { field: getTransactionDurationFieldForAggregatedTransactions( diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 2ed11480a7585..10aa56e79f06b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -40,7 +40,7 @@ export async function fetchAndTransformGcMetrics({ }) { const { start, end, apmEventClient, config } = setup; - const { bucketSize } = getBucketSize(start, end); + const { bucketSize } = getBucketSize({ start, end }); const projection = getMetricsProjection({ setup, diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 14245ce1d6c83..bcd6d10d31987 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -54,6 +54,6 @@ export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) { response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key, }; } catch (e) { - return false; + return { hasData: false, serviceName: undefined }; } } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts index 99d978116180b..0ca085105c30c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -43,7 +43,7 @@ export async function getServiceErrorGroups({ }) { const { apmEventClient, start, end, esFilter } = setup; - const { intervalString } = getBucketSize(start, end, numBuckets); + const { intervalString } = getBucketSize({ start, end, numBuckets }); const response = await apmEventClient.search({ apm: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts new file mode 100644 index 0000000000000..73b91429f5101 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; + +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; +import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; +import { getBucketSize } from '../../helpers/get_bucket_size'; + +export type TransactionGroupTimeseriesData = PromiseReturnType< + typeof getTimeseriesDataForTransactionGroups +>; + +export async function getTimeseriesDataForTransactionGroups({ + apmEventClient, + start, + end, + serviceName, + transactionNames, + esFilter, + searchAggregatedTransactions, + size, + numBuckets, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + serviceName: string; + transactionNames: string[]; + esFilter: ESFilter[]; + searchAggregatedTransactions: boolean; + size: number; + numBuckets: number; +}) { + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [TRANSACTION_NAME]: transactionNames } }, + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + transaction_groups: { + terms: { + field: TRANSACTION_NAME, + size, + }, + aggs: { + transaction_types: { + terms: { + field: TRANSACTION_TYPE, + }, + }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + avg_latency: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + [EVENT_OUTCOME]: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, + }, + }, + aggs: { + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + return timeseriesResponse.aggregations?.transaction_groups.buckets ?? []; +} 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 new file mode 100644 index 0000000000000..5d3d7014ba8f8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { orderBy } from 'lodash'; +import { ValuesType } from 'utility-types'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; +import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; + +export type ServiceOverviewTransactionGroupSortField = + | 'latency' + | 'throughput' + | 'errorRate' + | 'impact'; + +export type TransactionGroupWithoutTimeseriesData = ValuesType< + PromiseReturnType<typeof getTransactionGroupsForPage>['transactionGroups'] +>; + +export async function getTransactionGroupsForPage({ + apmEventClient, + searchAggregatedTransactions, + serviceName, + start, + end, + esFilter, + sortField, + sortDirection, + pageIndex, + size, +}: { + apmEventClient: APMEventClient; + searchAggregatedTransactions: boolean; + serviceName: string; + start: number; + end: number; + esFilter: ESFilter[]; + sortField: ServiceOverviewTransactionGroupSortField; + sortDirection: 'asc' | 'desc'; + pageIndex: number; + size: number; +}) { + const response = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + transaction_groups: { + terms: { + field: TRANSACTION_NAME, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + avg_latency: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + [EVENT_OUTCOME]: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, + }, + }, + aggs: { + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const transactionGroups = + response.aggregations?.transaction_groups.buckets.map((bucket) => { + const errorRate = + bucket.transaction_count.value > 0 + ? (bucket[EVENT_OUTCOME].transaction_count.value ?? 0) / + bucket.transaction_count.value + : null; + + return { + name: bucket.key as string, + latency: bucket.avg_latency.value, + throughput: bucket.transaction_count.value, + errorRate, + }; + }) ?? []; + + const totalDurationValues = transactionGroups.map( + (group) => (group.latency ?? 0) * group.throughput + ); + + const minTotalDuration = Math.min(...totalDurationValues); + const maxTotalDuration = Math.max(...totalDurationValues); + + const transactionGroupsWithImpact = transactionGroups.map((group) => ({ + ...group, + impact: + (((group.latency ?? 0) * group.throughput - minTotalDuration) / + (maxTotalDuration - minTotalDuration)) * + 100, + })); + + // Sort transaction groups first, and only get timeseries for data in view. + // This is to limit the possibility of creating too many buckets. + + const sortedAndSlicedTransactionGroups = orderBy( + transactionGroupsWithImpact, + sortField, + [sortDirection] + ).slice(pageIndex * size, pageIndex * size + size); + + return { + transactionGroups: sortedAndSlicedTransactionGroups, + totalTransactionGroups: transactionGroups.length, + isAggregationAccurate: + (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) === + 0, + }; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts new file mode 100644 index 0000000000000..88fd189712e07 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.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 { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getTimeseriesDataForTransactionGroups } from './get_timeseries_data_for_transaction_groups'; +import { + getTransactionGroupsForPage, + ServiceOverviewTransactionGroupSortField, +} from './get_transaction_groups_for_page'; +import { mergeTransactionGroupData } from './merge_transaction_group_data'; + +export async function getServiceTransactionGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, + searchAggregatedTransactions, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + size: number; + pageIndex: number; + numBuckets: number; + sortDirection: 'asc' | 'desc'; + sortField: ServiceOverviewTransactionGroupSortField; + searchAggregatedTransactions: boolean; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const { + transactionGroups, + totalTransactionGroups, + isAggregationAccurate, + } = await getTransactionGroupsForPage({ + apmEventClient, + start, + end, + serviceName, + esFilter, + pageIndex, + sortField, + sortDirection, + size, + searchAggregatedTransactions, + }); + + const transactionNames = transactionGroups.map((group) => group.name); + + const timeseriesData = await getTimeseriesDataForTransactionGroups({ + apmEventClient, + start, + end, + esFilter, + numBuckets, + searchAggregatedTransactions, + serviceName, + size, + transactionNames, + }); + + return { + transactionGroups: mergeTransactionGroupData({ + transactionGroups, + timeseriesData, + start, + end, + }), + totalTransactionGroups, + isAggregationAccurate, + }; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts new file mode 100644 index 0000000000000..f9266baddaf27 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; + +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../common/transaction_types'; + +import { TransactionGroupTimeseriesData } from './get_timeseries_data_for_transaction_groups'; + +import { TransactionGroupWithoutTimeseriesData } from './get_transaction_groups_for_page'; + +export function mergeTransactionGroupData({ + start, + end, + transactionGroups, + timeseriesData, +}: { + start: number; + end: number; + transactionGroups: TransactionGroupWithoutTimeseriesData[]; + timeseriesData: TransactionGroupTimeseriesData; +}) { + const deltaAsMinutes = (end - start) / 1000 / 60; + + return transactionGroups.map((transactionGroup) => { + const groupBucket = timeseriesData.find( + ({ key }) => key === transactionGroup.name + ); + + const transactionTypes = + groupBucket?.transaction_types.buckets.map( + (bucket) => bucket.key as string + ) ?? []; + + const transactionType = + transactionTypes.find( + (type) => type === TRANSACTION_PAGE_LOAD || type === TRANSACTION_REQUEST + ) ?? transactionTypes[0]; + + const timeseriesBuckets = groupBucket?.timeseries.buckets ?? []; + + return timeseriesBuckets.reduce( + (prev, point) => { + return { + ...prev, + latency: { + ...prev.latency, + timeseries: prev.latency.timeseries.concat({ + x: point.key, + y: point.avg_latency.value, + }), + }, + throughput: { + ...prev.throughput, + timeseries: prev.throughput.timeseries.concat({ + x: point.key, + y: point.transaction_count.value / deltaAsMinutes, + }), + }, + errorRate: { + ...prev.errorRate, + timeseries: prev.errorRate.timeseries.concat({ + x: point.key, + y: + point.transaction_count.value > 0 + ? (point[EVENT_OUTCOME].transaction_count.value ?? 0) / + point.transaction_count.value + : null, + }), + }, + }; + }, + { + name: transactionGroup.name, + transactionType, + latency: { + value: transactionGroup.latency, + timeseries: [] as Array<{ x: number; y: number | null }>, + }, + throughput: { + value: transactionGroup.throughput, + timeseries: [] as Array<{ x: number; y: number }>, + }, + errorRate: { + value: transactionGroup.errorRate, + timeseries: [] as Array<{ x: number; y: number | null }>, + }, + impact: transactionGroup.impact, + } + ); + }); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 89915e798b7cd..11f3e44fce87c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from '@kbn/logging'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { getServicesProjection } from '../../../projections/services'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -17,7 +16,6 @@ import { getTransactionRates, } from './get_services_items_stats'; -export type ServiceListAPIResponse = PromiseReturnType<typeof getServicesItems>; export type ServicesItemsSetup = Setup & SetupTimeRange; export type ServicesItemsProjection = ReturnType<typeof getServicesProjection>; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index fac80cf22c310..c8ebaa13d9df9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -37,7 +37,8 @@ import { function getDateHistogramOpts(start: number, end: number) { return { field: '@timestamp', - fixed_interval: getBucketSize(start, end, 20).intervalString, + fixed_interval: getBucketSize({ start, end, numBuckets: 20 }) + .intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 9d450804e421d..6a77392550bfe 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -6,14 +6,11 @@ import { Logger } from '@kbn/logging'; import { isEmpty } from 'lodash'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getLegacyDataStatus } from './get_legacy_data_status'; import { getServicesItems } from './get_services_items'; import { hasHistoricalAgentData } from './has_historical_agent_data'; -export type ServiceListAPIResponse = PromiseReturnType<typeof getServices>; - export async function getServices({ setup, searchAggregatedTransactions, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts index d76f9600b3d93..d68863e250684 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { convertConfigSettingsToString } from './convert_settings_to_string'; -export type AgentConfigurationListAPIResponse = PromiseReturnType< - typeof listConfigurations ->; export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_ranges.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_ranges.ts deleted file mode 100644 index 3cf0271baa1c6..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_ranges.ts +++ /dev/null @@ -1,90 +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 { rangeFilter } from '../../../../common/utils/range_filter'; -import { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getSignificantTermsAgg, - formatAggregationResponse, -} from './get_significant_terms_agg'; -import { SignificantTermsScoring } from './scoring_rt'; - -export async function getCorrelationsForRanges({ - serviceName, - transactionType, - transactionName, - scoring, - gapBetweenRanges, - fieldNames, - setup, -}: { - serviceName: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; - scoring: SignificantTermsScoring; - gapBetweenRanges: number; - fieldNames: string[]; - setup: Setup & SetupTimeRange; -}) { - const { start, end, esFilter, apmEventClient } = setup; - - const baseFilters = [...esFilter]; - - if (serviceName) { - baseFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - baseFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - baseFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - - const diff = end - start + gapBetweenRanges; - const baseRangeStart = start - diff; - const baseRangeEnd = end - diff; - const backgroundFilters = [ - ...baseFilters, - { range: rangeFilter(baseRangeStart, baseRangeEnd) }, - ]; - - const params = { - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { - bool: { filter: [...baseFilters, { range: rangeFilter(start, end) }] }, - }, - aggs: getSignificantTermsAgg({ - fieldNames, - backgroundFilters, - backgroundIsSuperset: false, - scoring, - }), - }, - }; - - const response = await apmEventClient.search(params); - - return { - message: `Showing significant fields between the ranges`, - firstRange: `${new Date(baseRangeStart).toISOString()} - ${new Date( - baseRangeEnd - ).toISOString()}`, - lastRange: `${new Date(start).toISOString()} - ${new Date( - end - ).toISOString()}`, - response: formatAggregationResponse(response.aggregations), - }; -} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts deleted file mode 100644 index c5ab8d8f1d111..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESFilter } from '../../../../../../typings/elasticsearch'; -import { SignificantTermsScoring } from './scoring_rt'; - -export function getSignificantTermsAgg({ - fieldNames, - backgroundFilters, - backgroundIsSuperset = true, - scoring = 'percentage', -}: { - fieldNames: string[]; - backgroundFilters: ESFilter[]; - backgroundIsSuperset?: boolean; - scoring: SignificantTermsScoring; -}) { - return fieldNames.reduce((acc, fieldName) => { - return { - ...acc, - [fieldName]: { - significant_terms: { - size: 10, - field: fieldName, - background_filter: { bool: { filter: backgroundFilters } }, - - // indicate whether background is a superset of the foreground - mutual_information: { background_is_superset: backgroundIsSuperset }, - - // different scorings https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html#significantterms-aggregation-parameters - [scoring]: {}, - min_doc_count: 5, - shard_min_doc_count: 5, - }, - }, - [`cardinality-${fieldName}`]: { - cardinality: { field: fieldName }, - }, - }; - }, {} as Record<string, any>); -} - -export function formatAggregationResponse(aggs?: Record<string, any>) { - if (!aggs) { - return; - } - - return Object.entries(aggs).reduce((acc, [key, value]) => { - if (key.startsWith('cardinality-')) { - if (value.value > 0) { - const fieldName = key.slice(12); - acc[fieldName] = { - ...acc[fieldName], - cardinality: value.value, - }; - } - } else if (value.buckets.length > 0) { - acc[key] = { - ...acc[key], - value, - }; - } - return acc; - }, {} as Record<string, { cardinality: number; value: any }>); -} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 89fff260a7d23..e57ea3aecb09a 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -204,7 +204,7 @@ export async function transactionGroupsFetcher( }; } -export interface TransactionGroup { +interface TransactionGroup { key: string | Record<'service.name' | 'transaction.name', string>; serviceName: string; transactionName: string; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index e9d273dad6262..dfd11203b87f1 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -78,7 +78,7 @@ export async function getErrorRate({ timeseries: { date_histogram: { field: '@timestamp', - fixed_interval: getBucketSize(start, end).intervalString, + fixed_interval: getBucketSize({ start, end }).intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index f11623eaa2dae..e72219a3cbd72 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -77,7 +77,7 @@ export async function getAnomalySeries({ return; } - const { intervalString, bucketSize } = getBucketSize(start, end); + const { intervalString, bucketSize } = getBucketSize({ start, end }); const esResponse = await anomalySeriesFetcher({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index a2da3977b81c7..cffec688806b5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -36,7 +36,7 @@ export function timeseriesFetcher({ searchAggregatedTransactions: boolean; }) { const { start, end, apmEventClient } = setup; - const { intervalString } = getBucketSize(start, end); + const { intervalString } = getBucketSize({ start, end }); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index c0421005dd06e..6c923290848a1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -17,7 +17,7 @@ export async function getApmTimeseriesData(options: { searchAggregatedTransactions: boolean; }) { const { start, end } = options.setup; - const { bucketSize } = getBucketSize(start, end); + const { bucketSize } = getBucketSize({ start, end }); const durationAsMinutes = (end - start) / 1000 / 60; const timeseriesResponse = await timeseriesFetcher(options); diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 010acd09239a3..98df68e50220d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -3,8 +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 { ValuesType } from 'utility-types'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; + import { SERVICE_NAME, TRACE_ID, @@ -196,7 +195,3 @@ export async function getBuckets({ buckets, }; } - -export type DistributionBucket = ValuesType< - PromiseReturnType<typeof getBuckets>['buckets'] ->; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts index deafc37ee42e2..af6e05a2ca336 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBuckets } from './get_buckets'; import { getDistributionMax } from './get_distribution_max'; @@ -18,9 +17,6 @@ function getBucketSize(max: number) { ); } -export type TransactionDistributionAPIResponse = PromiseReturnType< - typeof getTransactionDistribution ->; export async function getTransactionDistribution({ serviceName, transactionName, diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 99fb615d310db..6d1aead9292e3 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -6,21 +6,19 @@ import * as t from 'io-ts'; import { rangeRt } from './default_api_types'; -import { getCorrelationsForSlowTransactions } from '../lib/transaction_groups/correlations/get_correlations_for_slow_transactions'; -import { getCorrelationsForRanges } from '../lib/transaction_groups/correlations/get_correlations_for_ranges'; -import { scoringRt } from '../lib/transaction_groups/correlations/scoring_rt'; +import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions'; +import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_correlations_for_failed_transactions'; import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; export const correlationsForSlowTransactionsRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/slow_durations', + endpoint: 'GET /api/apm/correlations/slow_transactions', params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, transactionName: t.string, transactionType: t.string, - scoring: scoringRt, }), t.type({ durationPercentile: t.string, @@ -39,7 +37,6 @@ export const correlationsForSlowTransactionsRoute = createRoute({ transactionName, durationPercentile, fieldNames, - scoring = 'percentage', } = context.params.query; return getCorrelationsForSlowTransactions({ @@ -48,22 +45,19 @@ export const correlationsForSlowTransactionsRoute = createRoute({ transactionName, durationPercentile: parseInt(durationPercentile, 10), fieldNames: fieldNames.split(','), - scoring, setup, }); }, }); -export const correlationsForRangesRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/ranges', +export const correlationsForFailedTransactionsRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/failed_transactions', params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, transactionName: t.string, transactionType: t.string, - scoring: scoringRt, - gap: t.string, }), t.type({ fieldNames: t.string, @@ -75,27 +69,18 @@ export const correlationsForRangesRoute = createRoute({ options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { serviceName, transactionType, transactionName, - scoring = 'percentage', - gap, + fieldNames, } = context.params.query; - const gapBetweenRanges = parseInt(gap || '0', 10) * 3600 * 1000; - if (gapBetweenRanges < 0) { - throw new Error('gap must be 0 or positive'); - } - - return getCorrelationsForRanges({ + return getCorrelationsForFailedTransactions({ serviceName, transactionType, transactionName, - scoring, - gapBetweenRanges, fieldNames: fieldNames.split(','), setup, }); diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index a272b448deaf1..019482dd44485 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -22,6 +22,7 @@ import { serviceAnnotationsRoute, serviceAnnotationsCreateRoute, serviceErrorGroupsRoute, + serviceTransactionGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -43,8 +44,8 @@ import { serviceNodesRoute } from './service_nodes'; import { tracesRoute, tracesByIdRoute } from './traces'; import { transactionByTraceIdRoute } from './transaction'; import { - correlationsForRangesRoute, correlationsForSlowTransactionsRoute, + correlationsForFailedTransactionsRoute, } from './correlations'; import { transactionGroupsBreakdownRoute, @@ -116,6 +117,7 @@ const createApmApi = () => { .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) .add(serviceErrorGroupsRoute) + .add(serviceTransactionGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) @@ -129,7 +131,7 @@ const createApmApi = () => { // Correlations .add(correlationsForSlowTransactionsRoute) - .add(correlationsForRangesRoute) + .add(correlationsForFailedTransactionsRoute) // APM indices .add(apmIndexSettingsRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index e091e470b24b2..5e02fad2155ad 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -19,6 +19,7 @@ import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; +import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', @@ -233,7 +234,6 @@ export const serviceErrorGroupsRoute = createRoute({ path: { serviceName }, query: { size, numBuckets, pageIndex, sortDirection, sortField }, } = context.params; - return getServiceErrorGroups({ serviceName, setup, @@ -245,3 +245,52 @@ export const serviceErrorGroupsRoute = createRoute({ }); }, }); + +export const serviceTransactionGroupsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/overview_transaction_groups', + params: t.type({ + path: t.type({ serviceName: t.string }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('latency'), + t.literal('throughput'), + t.literal('errorRate'), + t.literal('impact'), + ]), + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceTransactionGroups({ + setup, + serviceName, + pageIndex, + searchAggregatedTransactions, + size, + sortDirection, + sortField, + numBuckets, + }); + }, +}); diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md index 002fbfb8b53f7..30011148cd1e7 100644 --- a/x-pack/plugins/case/README.md +++ b/x-pack/plugins/case/README.md @@ -57,11 +57,12 @@ This action type has no `secrets` properties. #### `subActionParams (addComment)` -| Property | Description | Type | -| -------- | --------------------------------------------------------- | ------ | -| comment | The case’s new comment. | string | -| type | The type of the comment, which can be: `user` or `alert`. | string | - +| Property | Description | Type | +| -------- | ----------------------------------------------------------------------- | ----------------- | +| type | The type of the comment | `user` \| `alert` | +| comment | The comment. Valid only when type is `user`. | string | +| alertId | The alert ID. Valid only when the type is `alert` | string | +| index | The index where the alert is saved. Valid only when the type is `alert` | string | #### `connector` | Property | Description | Type | diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index b4daac93940d8..920858a1e39b4 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -8,24 +8,33 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; -const CommentBasicRt = rt.type({ +export const CommentAttributesBasicRt = rt.type({ + created_at: rt.string, + created_by: UserRT, + pushed_at: rt.union([rt.string, rt.null]), + pushed_by: rt.union([UserRT, rt.null]), + updated_at: rt.union([rt.string, rt.null]), + updated_by: rt.union([UserRT, rt.null]), +}); + +export const ContextTypeUserRt = rt.type({ comment: rt.string, - type: rt.union([rt.literal('alert'), rt.literal('user')]), + type: rt.literal('user'), }); -export const CommentAttributesRt = rt.intersection([ - CommentBasicRt, - rt.type({ - created_at: rt.string, - created_by: UserRT, - pushed_at: rt.union([rt.string, rt.null]), - pushed_by: rt.union([UserRT, rt.null]), - updated_at: rt.union([rt.string, rt.null]), - updated_by: rt.union([UserRT, rt.null]), - }), -]); +export const ContextTypeAlertRt = rt.type({ + type: rt.literal('alert'), + alertId: rt.string, + index: rt.string, +}); -export const CommentRequestRt = CommentBasicRt; +const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); +const AttributesTypeAlertsRt = rt.intersection([ContextTypeAlertRt, CommentAttributesBasicRt]); +const CommentAttributesRt = rt.union([AttributesTypeUserRt, AttributesTypeAlertsRt]); + +const ContextBasicRt = rt.union([ContextTypeUserRt, ContextTypeAlertRt]); + +export const CommentRequestRt = ContextBasicRt; export const CommentResponseRt = rt.intersection([ CommentAttributesRt, @@ -38,10 +47,25 @@ export const CommentResponseRt = rt.intersection([ export const AllCommentsResponseRT = rt.array(CommentResponseRt); export const CommentPatchRequestRt = rt.intersection([ - rt.partial(CommentBasicRt.props), + /** + * Partial updates are not allowed. + * We want to prevent the user for changing the type without removing invalid fields. + */ + ContextBasicRt, rt.type({ id: rt.string, version: rt.string }), ]); +/** + * This type is used by the CaseService. + * Because the type for the attributes of savedObjectClient update function is Partial<T> + * we need to make all of our attributes partial too. + * We ensure that partial updates of CommentContext is not going to happen inside the patch comment route. + */ +export const CommentPatchAttributesRt = rt.intersection([ + rt.union([rt.partial(CommentAttributesBasicRt.props), rt.partial(ContextTypeAlertRt.props)]), + rt.partial(CommentAttributesBasicRt.props), +]); + export const CommentsResponseRt = rt.type({ comments: rt.array(CommentResponseRt), page: rt.number, @@ -62,3 +86,6 @@ export type CommentResponse = rt.TypeOf<typeof CommentResponseRt>; export type AllCommentsResponse = rt.TypeOf<typeof AllCommentsResponseRt>; export type CommentsResponse = rt.TypeOf<typeof CommentsResponseRt>; export type CommentPatchRequest = rt.TypeOf<typeof CommentPatchRequestRt>; +export type CommentPatchAttributes = rt.TypeOf<typeof CommentPatchAttributesRt>; +export type CommentRequestUserType = rt.TypeOf<typeof ContextTypeUserRt>; +export type CommentRequestAlertType = rt.TypeOf<typeof ContextTypeAlertRt>; diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index 50e104b30178a..d00df5a3246bd 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import { CommentType } from '../../../common/api'; import { createMockSavedObjectsRepository, @@ -31,7 +32,10 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, + comment: { + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user, + }, }); expect(res.id).toEqual('mock-id-1'); @@ -54,6 +58,43 @@ describe('addComment', () => { }); }); + test('it adds a comment of type alert correctly', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + }, + }); + + expect(res.id).toEqual('mock-id-1'); + expect(res.totalComment).toEqual(res.comments!.length); + expect(res.comments![res.comments!.length - 1]).toEqual({ + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + created_at: '2020-10-23T21:54:48.952Z', + created_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + test('it updates the case correctly after adding a comment', async () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, @@ -63,7 +104,10 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, + comment: { + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user, + }, }); expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z'); @@ -83,7 +127,10 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, + comment: { + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user, + }, }); expect( @@ -99,7 +146,7 @@ describe('addComment', () => { username: 'awesome', }, action_field: ['comment'], - new_value: 'Wow, good luck catching that bad meanie!', + new_value: '{"comment":"Wow, good luck catching that bad meanie!","type":"user"}', old_value: null, }, references: [ @@ -127,7 +174,10 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, + comment: { + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user, + }, }); expect(res.id).toEqual('mock-id-1'); @@ -151,7 +201,7 @@ describe('addComment', () => { }); describe('unhappy path', () => { - test('it throws when missing comment', async () => { + test('it throws when missing type', async () => { expect.assertions(3); const savedObjectsClient = createMockSavedObjectsRepository({ @@ -172,25 +222,126 @@ describe('addComment', () => { }); }); - test('it throws when missing comment type', async () => { + test('it throws when missing attributes: type user', async () => { expect.assertions(3); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client - .addComment({ - caseId: 'mock-id-1', - // @ts-expect-error - comment: { comment: 'a comment' }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + const allRequestAttributes = { + type: CommentType.user, + comment: 'a comment', + }; + + ['comment'].forEach((attribute) => { + const requestAttributes = omit(attribute, allRequestAttributes); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + // @ts-expect-error + comment: { + ...requestAttributes, + }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + }); + + test('it throws when excess attributes are provided: type user', async () => { + expect.assertions(6); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + + ['alertId', 'index'].forEach((attribute) => { + caseClient.client + .addComment({ + caseId: 'mock-id-1', + comment: { + [attribute]: attribute, + comment: 'a comment', + type: CommentType.user, + }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + }); + + test('it throws when missing attributes: type alert', async () => { + expect.assertions(6); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }; + + ['alertId', 'index'].forEach((attribute) => { + const requestAttributes = omit(attribute, allRequestAttributes); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + // @ts-expect-error + comment: { + ...requestAttributes, + }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + }); + + test('it throws when excess attributes are provided: type alert', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + + ['comment'].forEach((attribute) => { + caseClient.client + .addComment({ + caseId: 'mock-id-1', + comment: { + [attribute]: attribute, + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); }); test('it throws when the case does not exists', async () => { @@ -204,7 +355,10 @@ describe('addComment', () => { caseClient.client .addComment({ caseId: 'not-exists', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, + comment: { + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user, + }, }) .catch((e) => { expect(e).not.toBeNull(); @@ -224,7 +378,10 @@ describe('addComment', () => { caseClient.client .addComment({ caseId: 'mock-id-1', - comment: { comment: 'Throw an error', type: CommentType.user }, + comment: { + comment: 'Throw an error', + type: CommentType.user, + }, }) .catch((e) => { expect(e).not.toBeNull(); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index a95b7833a5232..169157c95d4c1 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -9,15 +9,9 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { flattenCaseSavedObject, transformNewComment } from '../../routes/api/utils'; +import { decodeComment, flattenCaseSavedObject, transformNewComment } from '../../routes/api/utils'; -import { - throwErrors, - excess, - CaseResponseRt, - CommentRequestRt, - CaseResponse, -} from '../../../common/api'; +import { throwErrors, CaseResponseRt, CommentRequestRt, CaseResponse } from '../../../common/api'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { CaseClientAddComment, CaseClientFactoryArguments } from '../types'; @@ -33,10 +27,13 @@ export const addComment = ({ comment, }: CaseClientAddComment): Promise<CaseResponse> => { const query = pipe( - excess(CommentRequestRt).decode(comment), + // TODO: Excess CommentRequestRt when the excess() function supports union types + CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) ); + decodeComment(comment); + const myCase = await caseService.getCase({ client: savedObjectsClient, caseId, @@ -105,7 +102,7 @@ export const addComment = ({ caseId: myCase.id, commentId: newComment.id, fields: ['comment'], - newValue: query.comment, + newValue: JSON.stringify(query), }), ], }), diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index e14281e047915..90bb1d604e733 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsMock } from '../../../../actions/server/mocks'; @@ -614,12 +615,31 @@ describe('case connector', () => { }); describe('add comment', () => { - it('succeeds when params is valid', () => { + it('succeeds when type is user', () => { + const params: Record<string, unknown> = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + comment: { + comment: 'a comment', + type: CommentType.user, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('succeeds when type is an alert', () => { const params: Record<string, unknown> = { subAction: 'addComment', subActionParams: { caseId: 'case-id', - comment: { comment: 'a comment', type: CommentType.user }, + comment: { + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + }, }, }; @@ -635,6 +655,89 @@ describe('case connector', () => { validateParams(caseActionType, params); }).toThrow(); }); + + it('fails when missing attributes: type user', () => { + const allParams = { + type: CommentType.user, + comment: 'a comment', + }; + + ['comment'].forEach((attribute) => { + const comment = omit(attribute, allParams); + const params: Record<string, unknown> = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + comment, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); + + it('fails when missing attributes: type alert', () => { + const allParams = { + type: CommentType.alert, + comment: 'a comment', + alertId: 'test-id', + index: 'test-index', + }; + + ['alertId', 'index'].forEach((attribute) => { + const comment = omit(attribute, allParams); + const params: Record<string, unknown> = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + comment, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); + + it('fails when excess attributes are provided: type user', () => { + ['alertId', 'index'].forEach((attribute) => { + const params: Record<string, unknown> = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + [attribute]: attribute, + type: CommentType.user, + comment: 'a comment', + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); + + it('fails when excess attributes are provided: type alert', () => { + ['comment'].forEach((attribute) => { + const params: Record<string, unknown> = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + [attribute]: attribute, + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); }); }); @@ -866,7 +969,10 @@ describe('case connector', () => { subAction: 'addComment', subActionParams: { caseId: 'case-id', - comment: { comment: 'a comment', type: CommentType.user }, + comment: { + comment: 'a comment', + type: CommentType.user, + }, }, }; @@ -883,7 +989,10 @@ describe('case connector', () => { expect(result).toEqual({ actionId, status: 'ok', data: commentReturn }); expect(mockCaseClient.addComment).toHaveBeenCalledWith({ caseId: 'case-id', - comment: { comment: 'a comment', type: CommentType.user }, + comment: { + comment: 'a comment', + type: CommentType.user, + }, }); }); }); diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index aa503e96be30d..039c0e2e7e67f 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -9,10 +9,18 @@ import { validateConnector } from './validators'; // Reserved for future implementation export const CaseConfigurationSchema = schema.object({}); -const CommentProps = { +const ContextTypeUserSchema = schema.object({ + type: schema.literal('user'), comment: schema.string(), - type: schema.oneOf([schema.literal('alert'), schema.literal('user')]), -}; +}); + +const ContextTypeAlertSchema = schema.object({ + type: schema.literal('alert'), + alertId: schema.string(), + index: schema.string(), +}); + +export const CommentSchema = schema.oneOf([ContextTypeUserSchema, ContextTypeAlertSchema]); const JiraFieldsSchema = schema.object({ issueType: schema.string(), @@ -86,7 +94,7 @@ const CaseUpdateRequestProps = { const CaseAddCommentRequestProps = { caseId: schema.string(), - comment: schema.object(CommentProps), + comment: CommentSchema, }; export const ExecutorSubActionCreateParamsSchema = schema.object(CaseBasicProps); diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index b3a05163fa6f4..da15f64a5718f 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -13,11 +13,13 @@ import { CaseConfigurationSchema, ExecutorSubActionAddCommentParamsSchema, ConnectorSchema, + CommentSchema, } from './schema'; import { CaseResponse, CasesResponse } from '../../../common/api'; export type CaseConfiguration = TypeOf<typeof CaseConfigurationSchema>; export type Connector = TypeOf<typeof ConnectorSchema>; +export type Comment = TypeOf<typeof CommentSchema>; export type ExecutorSubActionCreateParams = TypeOf<typeof ExecutorSubActionCreateParamsSchema>; export type ExecutorSubActionUpdateParams = TypeOf<typeof ExecutorSubActionUpdateParamsSchema>; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 9314ebb445820..4c0b5887ca998 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -297,6 +297,38 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [ updated_at: '2019-11-25T22:32:30.608Z', version: 'WzYsMV0=', }, + { + type: 'cases-comment', + id: 'mock-comment-4', + attributes: { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + created_at: '2019-11-25T22:32:30.608Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T22:32:30.608Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references: [ + { + type: 'cases', + name: 'associated-cases', + id: 'mock-id-4', + }, + ], + updated_at: '2019-11-25T22:32:30.608Z', + version: 'WzYsMV0=', + }, ]; export const mockCaseConfigure: Array<SavedObject<ESCasesConfigureAttributes>> = [ diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 400e8ca404ca5..5cb411f17a744 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { omit } from 'lodash/fp'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; @@ -15,12 +17,14 @@ import { } from '../../__fixtures__'; import { initPatchCommentApi } from './patch_comment'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CommentType } from '../../../../../common/api'; describe('PATCH comment', () => { let routeHandler: RequestHandler<any, any, any>; beforeAll(async () => { routeHandler = await createRoute(initPatchCommentApi, 'patch'); }); + it(`Patch a comment`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -29,6 +33,7 @@ describe('PATCH comment', () => { case_id: 'mock-id-1', }, body: { + type: CommentType.user, comment: 'Update my comment', id: 'mock-comment-1', version: 'WzEsMV0=', @@ -49,6 +54,183 @@ describe('PATCH comment', () => { ); }); + it(`Patch an alert`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'patch', + params: { + case_id: 'mock-id-4', + }, + body: { + type: CommentType.alert, + alertId: 'new-id', + index: 'test-index', + id: 'mock-comment-4', + version: 'WzYsMV0=', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.comments[response.payload.comments.length - 1].alertId).toEqual( + 'new-id' + ); + }); + + it(`it throws when missing attributes: type user`, async () => { + const allRequestAttributes = { + type: CommentType.user, + comment: 'a comment', + }; + + for (const attribute of ['comment']) { + const requestAttributes = omit(attribute, allRequestAttributes); + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: requestAttributes, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it throws when excess attributes are provided: type user`, async () => { + for (const attribute of ['alertId', 'index']) { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: { + [attribute]: attribute, + comment: 'a comment', + type: CommentType.user, + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it throws when missing attributes: type alert`, async () => { + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: requestAttributes, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it throws when excess attributes are provided: type alert`, async () => { + for (const attribute of ['comment']) { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: { + [attribute]: attribute, + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it fails to change the type of the comment`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'patch', + params: { + case_id: 'mock-id-1', + }, + body: { + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + id: 'mock-comment-1', + version: 'WzEsMV0=', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + expect(response.payload.message).toEqual('You cannot change the type of the comment.'); + }); + it(`Fails with 409 if version does not match`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -57,6 +239,7 @@ describe('PATCH comment', () => { case_id: 'mock-id-1', }, body: { + type: CommentType.user, id: 'mock-comment-1', comment: 'Update my comment', version: 'badv=', @@ -73,6 +256,7 @@ describe('PATCH comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(409); }); + it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -81,6 +265,7 @@ describe('PATCH comment', () => { case_id: 'mock-id-1', }, body: { + type: CommentType.user, comment: 'Update my comment', id: 'mock-comment-does-not-exist', version: 'WzEsMV0=', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index e75e89fa207b9..82fe3fce67653 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; -import Boom from '@hapi/boom'; +import { pick } from 'lodash/fp'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; +import Boom from '@hapi/boom'; import { CommentPatchRequestRt, CaseResponseRt, throwErrors } from '../../../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError, flattenCaseSavedObject } from '../../utils'; +import { escapeHatch, wrapError, flattenCaseSavedObject, decodeComment } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initPatchCommentApi({ @@ -42,6 +43,9 @@ export function initPatchCommentApi({ fold(throwErrors(Boom.badRequest), identity) ); + const { id: queryCommentId, version: queryCommentVersion, ...queryRestAttributes } = query; + decodeComment(queryRestAttributes); + const myCase = await caseService.getCase({ client, caseId, @@ -49,19 +53,23 @@ export function initPatchCommentApi({ const myComment = await caseService.getComment({ client, - commentId: query.id, + commentId: queryCommentId, }); if (myComment == null) { - throw Boom.notFound(`This comment ${query.id} does not exist anymore.`); + throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); + } + + if (myComment.attributes.type !== queryRestAttributes.type) { + throw Boom.badRequest(`You cannot change the type of the comment.`); } const caseRef = myComment.references.find((c) => c.type === CASE_SAVED_OBJECT); if (caseRef == null || (caseRef != null && caseRef.id !== caseId)) { - throw Boom.notFound(`This comment ${query.id} does not exist in ${caseId}).`); + throw Boom.notFound(`This comment ${queryCommentId} does not exist in ${caseId}).`); } - if (query.version !== myComment.version) { + if (queryCommentVersion !== myComment.version) { throw Boom.conflict( 'This case has been updated. Please refresh before saving additional updates.' ); @@ -73,13 +81,13 @@ export function initPatchCommentApi({ const [updatedComment, updatedCase] = await Promise.all([ caseService.patchComment({ client, - commentId: query.id, + commentId: queryCommentId, updatedAttributes: { - comment: query.comment, + ...queryRestAttributes, updated_at: updatedDate, updated_by: { email, full_name, username }, }, - version: query.version, + version: queryCommentVersion, }), caseService.patchCase({ client, @@ -122,8 +130,12 @@ export function initPatchCommentApi({ caseId: request.params.case_id, commentId: updatedComment.id, fields: ['comment'], - newValue: query.comment, - oldValue: myComment.attributes.comment, + newValue: JSON.stringify(queryRestAttributes), + oldValue: JSON.stringify( + // We are interested only in ContextBasicRt attributes + // myComment.attribute contains also CommentAttributesBasicRt attributes + pick(Object.keys(queryRestAttributes), myComment.attributes) + ), }), ], }), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 0b733bb034f8c..2909aa40a4425 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; @@ -55,6 +56,174 @@ describe('POST comment', () => { ); }); + it(`Posts a new comment of type alert`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: { + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( + 'mock-comment' + ); + }); + + it(`it throws when missing type`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: {}, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + }); + + it(`it throws when missing attributes: type user`, async () => { + const allRequestAttributes = { + type: CommentType.user, + comment: 'a comment', + }; + + for (const attribute of ['comment']) { + const requestAttributes = omit(attribute, allRequestAttributes); + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: requestAttributes, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it throws when excess attributes are provided: type user`, async () => { + for (const attribute of ['alertId', 'index']) { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: { + [attribute]: attribute, + comment: 'a comment', + type: CommentType.user, + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it throws when missing attributes: type alert`, async () => { + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: requestAttributes, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + + it(`it throws when excess attributes are provided: type alert`, async () => { + for (const attribute of ['comment']) { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', + }, + body: { + [attribute]: attribute, + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + } + }); + it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 01de9abac16af..6e2dfdc59f1b1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -104,7 +104,7 @@ describe('GET case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.comments).toHaveLength(3); + expect(response.payload.comments).toHaveLength(4); }); it(`returns an error when thrown from getAllCaseComments`, async () => { diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index 80b65b54468fc..6ba2da111090f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -11,7 +11,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils'; +import { + flattenCaseSavedObject, + wrapError, + escapeHatch, + getCommentContextFromAttributes, +} from '../utils'; import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; @@ -164,6 +169,7 @@ export function initPushCaseUserActionApi({ ], }), ]); + return response.ok({ body: CaseResponseRt.encode( flattenCaseSavedObject({ @@ -183,6 +189,7 @@ export function initPushCaseUserActionApi({ attributes: { ...origComment.attributes, ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), }, version: updatedComment?.version ?? origComment.version, references: origComment?.references ?? [], diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index fc1086b03814b..a67bae5ed74dc 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -117,7 +117,7 @@ describe('Utils', () => { it('transforms correctly', () => { const comment = { comment: 'A comment', - type: CommentType.user, + type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', email: 'elastic@elastic.co', full_name: 'Elastic', @@ -140,7 +140,7 @@ describe('Utils', () => { it('transform correctly without optional fields', () => { const comment = { comment: 'A comment', - type: CommentType.user, + type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', }; @@ -161,7 +161,7 @@ describe('Utils', () => { it('transform correctly with optional fields as null', () => { const comment = { comment: 'A comment', - type: CommentType.user, + type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', email: null, full_name: null, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index f8fe149c2ff2f..589d7c02a7be6 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { badRequest, boomify, isBoom } from '@hapi/boom'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; import { schema } from '@kbn/config-schema'; -import { boomify, isBoom } from '@hapi/boom'; import { CustomHttpResponseOptions, ResponseError, @@ -23,6 +26,13 @@ import { ESCaseConnector, ESCaseAttributes, CommentRequest, + ContextTypeUserRt, + ContextTypeAlertRt, + CommentRequestUserType, + CommentRequestAlertType, + CommentType, + excess, + throwErrors, } from '../../../common/api'; import { transformESConnectorToCaseConnector } from './cases/helpers'; @@ -56,24 +66,22 @@ export const transformNewCase = ({ updated_by: null, }); -interface NewCommentArgs extends CommentRequest { +type NewCommentArgs = CommentRequest & { createdDate: string; email?: string | null; full_name?: string | null; username?: string | null; -} +}; export const transformNewComment = ({ - comment, - type, createdDate, email, // eslint-disable-next-line @typescript-eslint/naming-convention full_name, username, + ...comment }: NewCommentArgs): CommentAttributes => ({ - comment, - type, + ...comment, created_at: createdDate, created_by: { email, full_name, username }, pushed_at: null, @@ -178,3 +186,33 @@ export const sortToSnake = (sortField: string): SortFieldCase => { }; export const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +const isUserContext = (context: CommentRequest): context is CommentRequestUserType => { + return context.type === CommentType.user; +}; + +const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => { + return context.type === CommentType.alert; +}; + +export const decodeComment = (comment: CommentRequest) => { + if (isUserContext(comment)) { + pipe(excess(ContextTypeUserRt).decode(comment), fold(throwErrors(badRequest), identity)); + } else if (isAlertContext(comment)) { + pipe(excess(ContextTypeAlertRt).decode(comment), fold(throwErrors(badRequest), identity)); + } +}; + +export const getCommentContextFromAttributes = ( + attributes: CommentAttributes +): CommentRequestUserType | CommentRequestAlertType => + isUserContext(attributes) + ? { + type: CommentType.user, + comment: attributes.comment, + } + : { + type: CommentType.alert, + alertId: attributes.alertId, + index: attributes.index, + }; diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 87478eb23641f..8f398c63e01bd 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -21,6 +21,12 @@ export const caseCommentSavedObjectType: SavedObjectsType = { type: { type: 'keyword', }, + alertId: { + type: 'keyword', + }, + index: { + type: 'keyword', + }, created_at: { type: 'date', }, diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index cab8cb499c3fa..0ce2b196af471 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -23,6 +23,7 @@ import { CommentAttributes, SavedObjectFindOptions, User, + CommentPatchAttributes, } from '../../common/api'; import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../saved_object_types'; import { readReporters } from './reporters/read_reporters'; @@ -78,18 +79,15 @@ type PatchCaseArgs = PatchCase & ClientArgs; interface PatchCasesArgs extends ClientArgs { cases: PatchCase[]; } -interface UpdateCommentArgs extends ClientArgs { - commentId: string; - updatedAttributes: Partial<CommentAttributes>; - version?: string; -} interface PatchComment { commentId: string; - updatedAttributes: Partial<CommentAttributes>; + updatedAttributes: CommentPatchAttributes; version?: string; } +type UpdateCommentArgs = PatchComment & ClientArgs; + interface PatchComments extends ClientArgs { comments: PatchComment[]; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg new file mode 100644 index 0000000000000..f1267ae57f0bd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg @@ -0,0 +1 @@ +<svg fill="none" height="38" viewBox="0 0 40 38" width="40" xmlns="http://www.w3.org/2000/svg"><g fill="#98a2b3"><path d="m22.8644.135712c-.3582.285768-.2974.503496.6893 2.204498.4867.83689.9463 1.57173 1.0138 1.62616.1014.07484 1.149.09525 5.8598.09525h5.7313l.1757-.1701c.1689-.1769.1757-.20412.1757-1.32678 0-1.10225-.0068-1.15668-.1622-1.31317-.0116-.01169-.0215-.02252-.0308-.03263-.0106-.01158-.0206-.02251-.031-.03239-.1174-.11111-.3237-.12819-2.8325-.335865l-.4174-.034572c-.4024-.034327-.8644-.073271-1.3351-.112951-.9287-.078288-1.8916-.159461-2.4971-.213642-.4055-.033018-.928-.076631-1.4656-.12149-.6808-.056822-1.3857-.11565-1.9069-.157474-.4696-.040814-.9561-.081629-1.3616-.115644-.4055-.0340201-.7302-.0612599-.8755-.074868-.4799-.040824-.5542-.02721599-.7299.115668z"/><path d="m23.2429 6.04839c-1.3247.694-1.6086.87091-1.6627 1.0342-.054.14969-.0405.22454.0609.3402.1216.12928.2162.14289 1.2773.14289h1.149l.0406.73483c.1554 2.81009 2.0275 5.13699 4.7107 5.87189.5948.1633 1.8857.2177 2.548.1156 3.163-.5171 5.5083-3.3884 5.3326-6.54542-.0406-.77565-.2298-1.61255-.4867-2.15006l-.1622-.34701-5.6096-.01361-5.6029-.02041z"/><path d="m21.9587 16.962c.8178-.5443 1.8857-.973 2.8454-1.1294.0946-.0205 2.4804-.0341 5.3055-.0409 4.3863-.0068 5.123.0068 5.0757.0885-.0338.0544-2.6291 3.368-5.7718 7.3551l-5.7111 7.2599-.0338-3.6197-.0405-3.613-5.8665 8.2397-3.3252-.0068h-3.3252l1.9329-2.7284c.5901-.8363 1.5021-2.1249 2.4535-3.4692.7594-1.073 1.544-2.1816 2.21-3.1239 2.8791-4.0756 3.2779-4.5655 4.2511-5.2119z"/><path d="m37.3143 16.8736c-.196.2449-1.345 1.701-2.548 3.2319-.7493.9535-1.6113 2.0495-2.3036 2.9297l-1.0081 1.2819c-3.9403 5.0078-6.0828 7.7498-6.0828 7.7838 0 .0204 2.0479.0477 4.5553.0613l4.5621.0136v2.2861l-10.8206.0408v1.0887c0 1.1703.0609 1.4492.3718 1.8643.1014.1293.3311.3062.5069.3946.0171.0079.0333.0154.0498.0225.2941.1272.6432.1272 7.2495.1272h6.9749l.3244-.2041c.4122-.2654.6015-.5171.7367-.9594.1554-.5239.1554-16.3568.0067-17.2345-.1757-.9934-.5272-1.7282-1.1422-2.3814-.3852-.4151-.8583-.7961-.9935-.7961-.0473 0-.2433.2041-.4393.4491z"/><path d="m.196 19.1325c.38524-.7961.96648-1.2996 1.81131-1.5717.31765-.1021.64206-.1293 1.56124-.1293 1.2233-.0068 1.48689.0476 1.64234.347.04731.0884.07434 2.5855.07434 7.498v7.3619h7.27227c5.0757 0 7.3196.0273 7.4412.0817.3447.1565.4055.381.3988 1.6125 0 1.5446-.1284 2.0276-.7232 2.708-.3447.3879-.6488.5988-1.2098.8029-.4055.1565-.4393.1565-6.3531.1565-5.92051 0-5.94079 0-6.3801-.1565-.80427-.279-1.50041-.9662-1.764-1.7282-.07434-.2245-.13517-.6668-.15544-1.1635l-.0338-.7893-.82455-.0408c-.588-.0272-.93269-.0748-1.19627-.1769-.66235-.2585-1.257103-.8301-1.574758-1.5173l-.182482-.3946v-12.499z"/></g></svg> \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 1bd3cabb0227d..73e7f7ed701d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -88,6 +88,12 @@ export interface ContentSource { name: string; } +export interface SourceContentItem { + id: string; + last_updated: string; + [key: string]: string; +} + export interface ContentSourceDetails extends ContentSource { status: string; statusMessage: string; @@ -105,11 +111,23 @@ interface DescriptionList { description: string; } +export interface DocumentSummaryItem { + count: number; + type: string; +} + +interface SourceActivity { + details: string[]; + event: string; + time: string; + status: string; +} + export interface ContentSourceFullData extends ContentSourceDetails { - activities: object[]; + activities: SourceActivity[]; details: DescriptionList[]; - summary: object[]; - groups: object[]; + summary: DocumentSummaryItem[]; + groups: Group[]; custom: boolean; accessToken: string; key: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 24c3a8f8ddb3b..c8fabaac2a4d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -5,7 +5,6 @@ */ import React, { useEffect, useState, ChangeEvent } from 'react'; -import noSharedSourcesIcon from 'workplace_search/components/assets/shareCircle.svg'; import { useActions, useValues } from 'kea'; @@ -18,6 +17,7 @@ import { EuiPanel, EuiEmptyPrompt, } from '@elastic/eui'; +import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; import { AppLogic } from '../../../../app_logic'; import { ContentSection } from '../../../../components/shared/content_section'; 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 new file mode 100644 index 0000000000000..0155c07f4e0bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -0,0 +1,532 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { Link } from 'react-router-dom'; + +import { + EuiAvatar, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiIconTip, + EuiLink, + EuiPanel, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { + CUSTOM_SOURCE_DOCS_URL, + DOCUMENT_PERMISSIONS_DOCS_URL, + ENT_SEARCH_LICENSE_MANAGEMENT, + EXTERNAL_IDENTITIES_DOCS_URL, + SOURCE_CONTENT_PATH, + getContentSourcePath, + getGroupPath, +} 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'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { Loading } from '../../../../../applications/shared/loading'; + +import aclImage from '../../../assets/supports_acl.svg'; +import { SourceLogic } from '../source_logic'; + +export const Overview: React.FC = () => { + const { contentSource, dataLoading } = useValues(SourceLogic); + const { isOrganization } = useValues(AppLogic); + + const { + id, + summary, + documentCount, + activities, + groups, + details, + custom, + accessToken, + key, + licenseSupportsPermissions, + serviceTypeSupportsPermissions, + indexPermissions, + hasPermissions, + isFederatedSource, + } = contentSource; + + if (dataLoading) return <Loading />; + + const DocumentSummary = () => { + let totalDocuments = 0; + const tableContent = + summary && + summary.map((item, index) => { + totalDocuments += item.count; + return ( + item.count > 0 && ( + <EuiTableRow key={index}> + <EuiTableRowCell>{item.type}</EuiTableRowCell> + <EuiTableRowCell>{item.count.toLocaleString('en-US')}</EuiTableRowCell> + </EuiTableRow> + ) + ); + }); + + const emptyState = ( + <> + <EuiSpacer size="s" /> + <EuiPanel paddingSize="l" className="euiPanel--inset"> + <EuiEmptyPrompt + title={<h2>No content yet</h2>} + iconType="documents" + iconColor="subdued" + /> + </EuiPanel> + </> + ); + + return ( + <div className="content-section"> + <div className="section-header"> + <EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiTitle size="xs"> + <h4>Content summary</h4> + </EuiTitle> + </EuiFlexItem> + {totalDocuments > 0 && ( + <EuiFlexItem grow={false}> + <Link to={getContentSourcePath(SOURCE_CONTENT_PATH, id, isOrganization)}> + <EuiButtonEmpty data-test-subj="ManageSourceContentLink" size="s"> + Manage + </EuiButtonEmpty> + </Link> + </EuiFlexItem> + )} + </EuiFlexGroup> + </div> + <EuiSpacer size="s" /> + {!summary && <ComponentLoader text="Loading summary details..." />} + {!!summary && + (totalDocuments === 0 ? ( + emptyState + ) : ( + <EuiTable> + <EuiTableHeader> + <EuiTableHeaderCell>Content Type</EuiTableHeaderCell> + <EuiTableHeaderCell>Items</EuiTableHeaderCell> + </EuiTableHeader> + <EuiTableBody> + {tableContent} + <EuiTableRow> + <EuiTableRowCell> + {summary ? <strong>Total documents</strong> : 'Documents'} + </EuiTableRowCell> + <EuiTableRowCell> + {summary ? ( + <strong>{totalDocuments.toLocaleString('en-US')}</strong> + ) : ( + parseInt(documentCount, 10).toLocaleString('en-US') + )} + </EuiTableRowCell> + </EuiTableRow> + </EuiTableBody> + </EuiTable> + ))} + </div> + ); + }; + + const ActivitySummary = () => { + const emptyState = ( + <> + <EuiSpacer size="s" /> + <EuiPanel paddingSize="l" className="euiPanel--inset"> + <EuiEmptyPrompt + title={<h2>There is no recent activity</h2>} + iconType="clock" + iconColor="subdued" + /> + </EuiPanel> + </> + ); + + const activitiesTable = ( + <EuiTable> + <EuiTableHeader> + <EuiTableHeaderCell>Event</EuiTableHeaderCell> + {!custom && <EuiTableHeaderCell>Status</EuiTableHeaderCell>} + <EuiTableHeaderCell>Time</EuiTableHeaderCell> + </EuiTableHeader> + <EuiTableBody> + {activities.map(({ details: activityDetails, event, time, status }, i) => ( + <EuiTableRow key={i}> + <EuiTableRowCell> + <EuiText size="xs">{event}</EuiText> + </EuiTableRowCell> + {!custom && ( + <EuiTableRowCell> + <EuiText size="xs"> + {status}{' '} + {activityDetails && ( + <EuiIconTip + position="top" + content={activityDetails.map((detail, idx) => ( + <div key={idx}>{detail}</div> + ))} + /> + )} + </EuiText> + </EuiTableRowCell> + )} + <EuiTableRowCell> + <EuiText size="xs">{time}</EuiText> + </EuiTableRowCell> + </EuiTableRow> + ))} + </EuiTableBody> + </EuiTable> + ); + + return ( + <div className="content-section"> + <div className="section-header"> + <EuiTitle size="xs"> + <h3>Recent activity</h3> + </EuiTitle> + </div> + <EuiSpacer size="s" /> + {activities.length === 0 ? emptyState : activitiesTable} + </div> + ); + }; + + const GroupsSummary = () => { + const GroupAvatars = ({ users }: { users: User[] }) => { + const MAX_USERS = 4; + return ( + <EuiFlexGroup gutterSize="xs" alignItems="center"> + {users.slice(0, MAX_USERS).map((user) => ( + <EuiFlexItem key={user.id}> + <EuiAvatar + size="s" + initials={user.initials} + name={user.name || user.initials} + imageUrl={user.pictureUrl || ''} + /> + </EuiFlexItem> + ))} + {users.slice(MAX_USERS).length > 0 && ( + <EuiFlexItem> + <EuiText color="subdued" size="xs"> + <strong>+{users.slice(MAX_USERS).length}</strong> + </EuiText> + </EuiFlexItem> + )} + </EuiFlexGroup> + ); + }; + + return !groups.length ? null : ( + <EuiPanel> + <EuiText size="s"> + <h6> + <EuiTextColor color="subdued">Group Access</EuiTextColor> + </h6> + </EuiText> + <EuiSpacer size="s" /> + <EuiFlexGroup direction="column" gutterSize="s"> + {groups.map((group, index) => ( + <EuiFlexItem key={index}> + <Link to={getGroupPath(group.id)} data-test-subj="SourceGroupLink"> + <EuiPanel className="euiPanel--inset"> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem> + <EuiText size="s" className="eui-textTruncate"> + <strong>{group.name}</strong> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <GroupAvatars users={group.users} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </Link> + </EuiFlexItem> + ))} + </EuiFlexGroup> + </EuiPanel> + ); + }; + + const detailsSummary = ( + <EuiPanel> + <EuiText size="s"> + <h6> + <EuiTextColor color="subdued">Configuration</EuiTextColor> + </h6> + </EuiText> + <EuiSpacer /> + <EuiText size="s"> + {details.map((detail, index) => ( + <EuiFlexGroup + wrap + gutterSize="s" + alignItems="center" + justifyContent="spaceBetween" + key={index} + > + <EuiFlexItem grow={false}> + <strong>{detail.title}</strong> + </EuiFlexItem> + <EuiFlexItem grow={false}>{detail.description}</EuiFlexItem> + </EuiFlexGroup> + ))} + </EuiText> + </EuiPanel> + ); + + const documentPermissions = ( + <> + <EuiSpacer /> + <EuiTitle size="s"> + <h4>Document-level permissions</h4> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiPanel> + <EuiFlexGroup gutterSize="m" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiIcon type={aclImage} size="l" color="primary" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText> + <strong>Using document-level permissions</strong> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </> + ); + + const documentPermissionsDisabled = ( + <> + <EuiSpacer /> + <EuiTitle size="s"> + <h4>Document-level permissions</h4> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiPanel className="euiPanel--inset"> + <EuiText size="s"> + <EuiFlexGroup wrap gutterSize="m" alignItems="center" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiIcon size="l" type="iInCircle" color="subdued" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText size="m"> + <strong>Disabled for this source</strong> + </EuiText> + <EuiText size="s"> + <EuiLink target="_blank" href={DOCUMENT_PERMISSIONS_DOCS_URL}> + Learn more + </EuiLink>{' '} + about permissions + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiText> + </EuiPanel> + </> + ); + + const sourceStatus = ( + <EuiPanel> + <EuiText size="s"> + <h6> + <EuiTextColor color="subdued">Status</EuiTextColor> + </h6> + </EuiText> + <EuiSpacer size="s" /> + <EuiFlexGroup gutterSize="m" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiIcon size="l" type="checkInCircleFilled" color="secondary" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText> + <strong>Everything looks good</strong> + </EuiText> + <EuiText size="s"> + <p>Your endpoints are ready to accept requests.</p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); + + const permissionsStatus = ( + <EuiPanel> + <EuiText size="s"> + <h6> + <EuiTextColor color="subdued">Status</EuiTextColor> + </h6> + </EuiText> + <EuiSpacer size="s" /> + <EuiFlexGroup gutterSize="m" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiIcon size="xl" type="dot" color="warning" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText> + <strong>Requires additional configuration</strong> + </EuiText> + <EuiText size="s"> + <p> + The{' '} + <EuiLink target="_blank" href={EXTERNAL_IDENTITIES_DOCS_URL}> + External Identities API + </EuiLink>{' '} + must be used to configure user access mappings. Read the guide to learn more. + </p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); + + const credentials = ( + <EuiPanel> + <EuiText size="s"> + <h6> + <EuiTextColor color="subdued">Credentials</EuiTextColor> + </h6> + </EuiText> + <EuiSpacer size="s" /> + <CredentialItem label="Access Token" value={accessToken} testSubj="AccessToken" /> + <EuiSpacer size="s" /> + <CredentialItem label="Key" value={key} testSubj="ContentSourceKey" /> + </EuiPanel> + ); + + const DocumentationCallout = ({ + title, + children, + }: { + title: string; + children: React.ReactNode; + }) => ( + <EuiPanel> + <EuiText size="s"> + <h6> + <EuiTextColor color="subdued">Documentation</EuiTextColor> + </h6> + </EuiText> + <EuiSpacer size="s" /> + <EuiTitle size="xs"> + <h4>{title}</h4> + </EuiTitle> + <EuiText size="s">{children}</EuiText> + </EuiPanel> + ); + + const documentPermssionsLicenseLocked = ( + <EuiPanel> + <LicenseBadge /> + <EuiSpacer size="s" /> + <EuiTitle size="xs"> + <h4>Document-level permissions</h4> + </EuiTitle> + <EuiText size="s"> + <p> + Document-level permissions manage content access content on individual or group + attributes. Allow or deny access to specific documents. + </p> + </EuiText> + <EuiSpacer size="s" /> + <EuiText size="s"> + <EuiLink target="_blank" href={ENT_SEARCH_LICENSE_MANAGEMENT}> + Learn about Platinum features + </EuiLink> + </EuiText> + </EuiPanel> + ); + + return ( + <> + <ViewContentHeader title="Source overview" /> + <EuiSpacer size="m" /> + <EuiFlexGroup gutterSize="xl" alignItems="flexStart"> + <EuiFlexItem> + <EuiFlexGroup gutterSize="xl" direction="column"> + <EuiFlexItem> + <DocumentSummary /> + </EuiFlexItem> + {!isFederatedSource && ( + <EuiFlexItem> + <ActivitySummary /> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup gutterSize="m" direction="column"> + <EuiFlexItem> + <GroupsSummary /> + </EuiFlexItem> + {details.length > 0 && <EuiFlexItem>{detailsSummary}</EuiFlexItem>} + {!custom && serviceTypeSupportsPermissions && ( + <> + {indexPermissions && !hasPermissions && ( + <EuiFlexItem>{permissionsStatus}</EuiFlexItem> + )} + {indexPermissions && <EuiFlexItem>{documentPermissions}</EuiFlexItem>} + {!indexPermissions && isOrganization && ( + <EuiFlexItem>{documentPermissionsDisabled}</EuiFlexItem> + )} + {indexPermissions && <EuiFlexItem>{credentials}</EuiFlexItem>} + </> + )} + {custom && ( + <> + <EuiFlexItem>{sourceStatus}</EuiFlexItem> + <EuiFlexItem>{credentials}</EuiFlexItem> + <EuiFlexItem> + <DocumentationCallout title="Getting started with custom sources?"> + <p> + <EuiLink target="_blank" href={CUSTOM_SOURCE_DOCS_URL}> + Learn more + </EuiLink>{' '} + about custom sources. + </p> + </DocumentationCallout> + </EuiFlexItem> + {!licenseSupportsPermissions && ( + <EuiFlexItem>{documentPermssionsLicenseLocked}</EuiFlexItem> + )} + </> + )} + </EuiFlexGroup> + </EuiFlexItem> + <EuiEmptyPrompt /> + </EuiFlexGroup> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx new file mode 100644 index 0000000000000..16aceacbddcd5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Location } from 'history'; +import { useActions, useValues } from 'kea'; +import { Redirect, useLocation } from 'react-router-dom'; + +import { setErrorMessage } from '../../../../shared/flash_messages'; + +import { parseQueryParams } from '../../../../../applications/shared/query_params'; + +import { SOURCES_PATH, getSourcesPath } from '../../../routes'; + +import { AppLogic } from '../../../app_logic'; +import { SourcesLogic } from '../sources_logic'; + +interface SourceQueryParams { + name: string; + hasError: boolean; + errorMessages?: string[]; + serviceType: string; + indexPermissions: boolean; +} + +export const SourceAdded: React.FC = () => { + const { search } = useLocation() as Location; + const { name, hasError, errorMessages, serviceType, indexPermissions } = (parseQueryParams( + search + ) as unknown) as SourceQueryParams; + const { setAddedSource } = useActions(SourcesLogic); + const { isOrganization } = useValues(AppLogic); + const decodedName = decodeURIComponent(name); + + if (hasError) { + const defaultError = `${decodedName} failed to connect.`; + setErrorMessage(errorMessages ? errorMessages.join(' ') : defaultError); + } else { + setAddedSource(decodedName, indexPermissions, serviceType); + } + + return <Redirect to={getSourcesPath(SOURCES_PATH, isOrganization)} />; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx new file mode 100644 index 0000000000000..3f289a6394131 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { useActions, useValues } from 'kea'; +import { startCase } from 'lodash'; +import moment from 'moment'; + +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiLink, +} from '@elastic/eui'; + +import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; +import { SourceContentItem } from '../../../types'; + +import { TruncatedContent } from '../../../../shared/truncate'; + +const MAX_LENGTH = 28; + +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { Loading } from '../../../../../applications/shared/loading'; +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { CUSTOM_SERVICE_TYPE } from '../../../constants'; + +import { SourceLogic } from '../source_logic'; + +export const SourceContent: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + + const { + setActivePage, + searchContentSourceDocuments, + resetSourceState, + setContentFilterValue, + } = useActions(SourceLogic); + + const { + contentSource: { id, serviceType, urlField, titleField, urlFieldIsLinkable, isFederatedSource }, + contentMeta: { + page: { total_pages: totalPages, total_results: totalItems, current: activePage }, + }, + contentItems, + contentFilterValue, + dataLoading, + sectionLoading, + } = useValues(SourceLogic); + + useEffect(() => { + return resetSourceState; + }, []); + + useEffect(() => { + searchContentSourceDocuments(id); + }, [contentFilterValue, activePage]); + + if (dataLoading) return <Loading />; + + const showPagination = totalPages > 1; + const hasItems = totalItems > 0; + const emptyMessage = contentFilterValue + ? `No results for '${contentFilterValue}'` + : "This source doesn't have any content yet"; + + const paginationOptions = { + totalPages, + totalItems, + activePage, + onChangePage: (page: number) => { + // EUI component starts page at 0. API starts at 1. + setActivePage(page + 1); + }, + }; + + const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; + + const emptyState = ( + <EuiPanel className="euiPanel--inset"> + <EuiSpacer size="xxl" /> + <EuiPanel className="euiPanel--inset"> + <EuiEmptyPrompt + title={<h2>{emptyMessage}</h2>} + iconType="documents" + body={ + isCustomSource ? ( + <p> + Learn more about adding content in our{' '} + <EuiLink target="_blank" href={CUSTOM_SOURCE_DOCS_URL}> + documentation + </EuiLink> + </p> + ) : null + } + /> + </EuiPanel> + <EuiSpacer size="l" /> + </EuiPanel> + ); + + const contentItem = (item: SourceContentItem) => { + const { id: itemId, last_updated: updated } = item; + const url = item[urlField] || ''; + const title = item[titleField] || ''; + + return ( + <EuiTableRow key={itemId} data-test-subj="ContentItemRow"> + <EuiTableRowCell className="eui-textTruncate"> + <TruncatedContent tooltipType="title" content={title.toString()} length={MAX_LENGTH} /> + </EuiTableRowCell> + <EuiTableRowCell className="eui-textTruncate"> + {!urlFieldIsLinkable && ( + <TruncatedContent tooltipType="title" content={url.toString()} length={MAX_LENGTH} /> + )} + {urlFieldIsLinkable && ( + <EuiLink target="_blank" href={url}> + <TruncatedContent tooltipType="title" content={url.toString()} length={MAX_LENGTH} /> + </EuiLink> + )} + </EuiTableRowCell> + <EuiTableRowCell>{moment(updated).format('M/D/YYYY, h:mm:ss A')}</EuiTableRowCell> + </EuiTableRow> + ); + }; + + const contentTable = ( + <> + {showPagination && <TablePaginationBar {...paginationOptions} />} + <EuiSpacer size="m" /> + <EuiTable> + <EuiTableHeader> + <EuiTableHeaderCell>Title</EuiTableHeaderCell> + <EuiTableHeaderCell>{startCase(urlField)}</EuiTableHeaderCell> + <EuiTableHeaderCell>Last Updated</EuiTableHeaderCell> + </EuiTableHeader> + <EuiTableBody>{contentItems.map(contentItem)}</EuiTableBody> + </EuiTable> + <EuiSpacer size="m" /> + {showPagination && <TablePaginationBar {...paginationOptions} hideLabelCount />} + </> + ); + + const resetFederatedSearchTerm = () => { + setContentFilterValue(''); + setSearchTerm(''); + }; + const federatedSearchControls = ( + <> + <EuiFlexItem grow={false}> + <EuiButton + disabled={!searchTerm} + fill + color="primary" + onClick={() => setContentFilterValue(searchTerm)} + > + Go + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty disabled={!searchTerm} onClick={resetFederatedSearchTerm}> + Reset + </EuiButtonEmpty> + </EuiFlexItem> + </> + ); + + return ( + <> + <ViewContentHeader title="Source content" /> + <EuiSpacer size="l" /> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiFieldSearch + disabled={!hasItems && !contentFilterValue} + placeholder={`${isFederatedSource ? 'Search' : 'Filter'} content...`} + incremental={!isFederatedSource} + isClearable={!isFederatedSource} + onSearch={setContentFilterValue} + data-test-subj="ContentFilterInput" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + /> + </EuiFlexItem> + {isFederatedSource && federatedSearchControls} + </EuiFlexGroup> + <EuiSpacer size="xl" /> + {sectionLoading && <ComponentLoader text="Loading content..." />} + {!sectionLoading && (hasItems ? contentTable : emptyState)} + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx new file mode 100644 index 0000000000000..e3c3e76311018 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSpacer, +} from '@elastic/eui'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +interface SourceInfoCardProps { + sourceName: string; + sourceType: string; + dateCreated: string; + isFederatedSource: boolean; +} + +export const SourceInfoCard: React.FC<SourceInfoCardProps> = ({ + sourceName, + sourceType, + dateCreated, + isFederatedSource, +}) => ( + <EuiFlexGroup gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiDescriptionList textStyle="reverse" className="content-source-meta"> + <EuiDescriptionListTitle> + <span className="content-source-meta__title">Connector</span> + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <EuiFlexGroup + gutterSize="xs" + alignItems="center" + className="content-source-meta__content" + > + <EuiFlexItem grow={false}> + <SourceIcon + className="content-source-meta__icon" + serviceType={sourceType} + name={sourceType} + /> + </EuiFlexItem> + <EuiFlexItem> + <span title={sourceName} className="eui-textTruncate"> + {sourceName} + </span> + </EuiFlexItem> + </EuiFlexGroup> + </EuiDescriptionListDescription> + </EuiDescriptionList> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSpacer className="euiSpacer--vertical" /> + </EuiFlexItem> + <EuiFlexItem grow={isFederatedSource}> + <EuiDescriptionList textStyle="reverse" className="content-source-meta"> + <EuiDescriptionListTitle> + <span className="content-source-meta__title">Created</span> + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <EuiFlexGroup + gutterSize="xs" + alignItems="center" + className="content-source-meta__content" + > + <EuiFlexItem>{dateCreated}</EuiFlexItem> + </EuiFlexGroup> + </EuiDescriptionListDescription> + </EuiDescriptionList> + </EuiFlexItem> + {isFederatedSource && ( + <> + <EuiFlexItem grow={false}> + <EuiSpacer className="euiSpacer--vertical" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiDescriptionList textStyle="reverse" className="content-source-meta"> + <EuiDescriptionListTitle> + <span className="content-source-meta__title">Status</span> + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <EuiFlexGroup + gutterSize="xs" + alignItems="center" + className="content-source-meta__content" + > + <EuiFlexItem> + <EuiHealth color="success">Ready to search</EuiHealth> + </EuiFlexItem> + </EuiFlexGroup> + </EuiDescriptionListDescription> + </EuiDescriptionList> + </EuiFlexItem> + </> + )} + </EuiFlexGroup> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx new file mode 100644 index 0000000000000..1f756115e3ae4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, ChangeEvent, FormEvent } from 'react'; + +import { History } from 'history'; +import { useActions, useValues } from 'kea'; +import { isEmpty } from 'lodash'; +import { Link, useHistory } from 'react-router-dom'; + +import { + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; + +import { SOURCES_PATH, getSourcesPath } from '../../../routes'; + +import { ContentSection } from '../../../components/shared/content_section'; +import { SourceConfigFields } from '../../../components/shared/source_config_fields'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { SourceDataItem } from '../../../types'; +import { AppLogic } from '../../../app_logic'; +import { staticSourceData } from '../source_data'; + +import { SourceLogic } from '../source_logic'; + +export const SourceSettings: React.FC = () => { + const history = useHistory() as History; + const { + updateContentSource, + removeContentSource, + resetSourceState, + getSourceConfigData, + } = useActions(SourceLogic); + + const { + contentSource: { name, id, serviceType }, + buttonLoading, + sourceConfigData: { configuredFields }, + } = useValues(SourceLogic); + + const { isOrganization } = useValues(AppLogic); + + useEffect(() => { + getSourceConfigData(serviceType); + return resetSourceState; + }, []); + const { + configuration: { isPublicKey }, + editPath, + } = staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem; + + const [inputValue, setValue] = useState(name); + const [confirmModalVisible, setModalVisibility] = useState(false); + const showConfirm = () => setModalVisibility(true); + const hideConfirm = () => setModalVisibility(false); + + const showConfig = isOrganization && !isEmpty(configuredFields); + + const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; + + const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value); + + const submitNameChange = (e: FormEvent) => { + e.preventDefault(); + updateContentSource(id, { name: inputValue }); + }; + + const handleSourceRemoval = () => { + /** + * The modal was just hanging while the UI waited for the server to respond. + * EuiModal doens't allow the button to have a loading state so we just hide the + * modal here and set the button that was clicked to delete to a loading state. + */ + setModalVisibility(false); + const onSourceRemoved = () => history.push(getSourcesPath(SOURCES_PATH, isOrganization)); + removeContentSource(id, onSourceRemoved); + }; + + const confirmModal = ( + <EuiOverlayMask> + <EuiConfirmModal + title="Please confirm" + onConfirm={handleSourceRemoval} + onCancel={hideConfirm} + buttonColor="danger" + cancelButtonText="Cancel" + confirmButtonText="Ok" + defaultFocusedButton="confirm" + > + Your source documents will be deleted from Workplace Search. <br /> + Are you sure you want to remove {name}? + </EuiConfirmModal> + </EuiOverlayMask> + ); + + return ( + <> + <ViewContentHeader title="Source settings" /> + <EuiSpacer /> + <ContentSection + title="Content source name" + description="Customize the name of this content source." + > + <form onSubmit={submitNameChange}> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiFormRow> + <EuiFieldText + value={inputValue} + size={64} + onChange={handleNameChange} + aria-label="Source Name" + disabled={buttonLoading} + data-test-subj="SourceNameInput" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + disabled={buttonLoading} + color="primary" + onClick={submitNameChange} + data-test-subj="SaveChangesButton" + > + Save changes + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </form> + </ContentSection> + {showConfig && ( + <ContentSection + title="Content source configuration" + description="Edit content source connector settings to change." + > + <SourceConfigFields + clientId={clientId} + clientSecret={clientSecret} + publicKey={isPublicKey ? publicKey : undefined} + consumerKey={consumerKey || undefined} + baseUrl={baseUrl} + /> + <EuiFormRow> + <Link to={editPath}> + <EuiButtonEmpty flush="left">Edit content source connector settings</EuiButtonEmpty> + </Link> + </EuiFormRow> + </ContentSection> + )} + <ContentSection title="Remove this source" description="This action cannot be undone."> + <EuiButton + isLoading={buttonLoading} + data-test-subj="DeleteSourceButton" + fill + color="danger" + onClick={showConfirm} + > + Remove + </EuiButton> + {confirmModalVisible && confirmModal} + </ContentSection> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 889519b8a9985..0a11da02dc789 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -23,7 +23,13 @@ import { import { DEFAULT_META } from '../../../shared/constants'; import { AppLogic } from '../../app_logic'; import { NOT_FOUND_PATH } from '../../routes'; -import { ContentSourceFullData, CustomSource, Meta } from '../../types'; +import { + ContentSourceFullData, + CustomSource, + Meta, + DocumentSummaryItem, + SourceContentItem, +} from '../../types'; export interface SourceActions { onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData; @@ -32,7 +38,7 @@ export interface SourceActions { setSourceConnectData(sourceConnectData: SourceConnectData): SourceConnectData; setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse; initializeFederatedSummary(sourceId: string): { sourceId: string }; - onUpdateSummary(summary: object[]): object[]; + onUpdateSummary(summary: DocumentSummaryItem[]): DocumentSummaryItem[]; setContentFilterValue(contentFilterValue: string): string; setActivePage(activePage: number): number; setClientIdValue(clientIdValue: string): string; @@ -108,7 +114,7 @@ interface SourceValues { dataLoading: boolean; sectionLoading: boolean; buttonLoading: boolean; - contentItems: object[]; + contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; customSourceNameValue: string; @@ -129,7 +135,7 @@ interface SourceValues { } interface SearchResultsResponse { - results: object[]; + results: SourceContentItem[]; meta: Meta; } 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 bd57958b0cb88..c1f60f2d63049 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 @@ -9,6 +9,7 @@ import { IClusterClientAdapter } from './cluster_client_adapter'; const createClusterClientMock = () => { const mock: jest.Mocked<IClusterClientAdapter> = { indexDocument: jest.fn(), + indexDocuments: jest.fn(), doesIlmPolicyExist: jest.fn(), createIlmPolicy: jest.fn(), doesIndexTemplateExist: jest.fn(), @@ -16,6 +17,7 @@ const createClusterClientMock = () => { doesAliasExist: jest.fn(), createIndex: jest.fn(), queryEventsBySavedObject: 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 6e787c905d400..57a6b1d3bb932 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 @@ -4,14 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyClusterClient, Logger } from 'src/core/server'; +import { LegacyClusterClient } from 'src/core/server'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; -import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter'; +import { + ClusterClientAdapter, + IClusterClientAdapter, + EVENT_BUFFER_LENGTH, +} from './cluster_client_adapter'; +import { contextMock } from './context.mock'; import { findOptionsSchema } from '../event_log_client'; +import { delay } from '../lib/delay'; +import { times } from 'lodash'; type EsClusterClient = Pick<jest.Mocked<LegacyClusterClient>, 'callAsInternalUser' | 'asScoped'>; +type MockedLogger = ReturnType<typeof loggingSystemMock['createLogger']>; -let logger: Logger; +let logger: MockedLogger; let clusterClient: EsClusterClient; let clusterClientAdapter: IClusterClientAdapter; @@ -21,22 +29,130 @@ beforeEach(() => { clusterClientAdapter = new ClusterClientAdapter({ logger, clusterClientPromise: Promise.resolve(clusterClient), + context: contextMock.create(), }); }); describe('indexDocument', () => { - test('should call cluster client with given doc', async () => { - await clusterClientAdapter.indexDocument({ args: true }); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('index', { - args: true, + test('should call cluster client bulk with given doc', async () => { + clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); + + await retryUntil('cluster client bulk called', () => { + return clusterClient.callAsInternalUser.mock.calls.length !== 0; + }); + + expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', { + body: [{ create: { _index: 'event-log' } }, { message: 'foo' }], }); }); - test('should throw error when cluster client throws an error', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); - await expect( - clusterClientAdapter.indexDocument({ args: true }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + test('should log an error when cluster client throws an error', async () => { + clusterClient.callAsInternalUser.mockRejectedValue(new Error('expected failure')); + clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); + await retryUntil('cluster client bulk called', () => { + return logger.error.mock.calls.length !== 0; + }); + + const expectedMessage = `error writing bulk events: "expected failure"; docs: [{"create":{"_index":"event-log"}},{"message":"foo"}]`; + expect(logger.error).toHaveBeenCalledWith(expectedMessage); + }); +}); + +describe('shutdown()', () => { + test('should work if no docs have been written', async () => { + const result = await clusterClientAdapter.shutdown(); + expect(result).toBeFalsy(); + }); + + test('should work if some docs have been written', async () => { + clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); + const resultPromise = clusterClientAdapter.shutdown(); + + await retryUntil('cluster client bulk called', () => { + return clusterClient.callAsInternalUser.mock.calls.length !== 0; + }); + + const result = await resultPromise; + expect(result).toBeFalsy(); + }); +}); + +describe('buffering documents', () => { + test('should write buffered docs after timeout', async () => { + // write EVENT_BUFFER_LENGTH - 1 docs + for (let i = 0; i < EVENT_BUFFER_LENGTH - 1; i++) { + clusterClientAdapter.indexDocument({ body: { message: `foo ${i}` }, index: 'event-log' }); + } + + await retryUntil('cluster client bulk called', () => { + return clusterClient.callAsInternalUser.mock.calls.length !== 0; + }); + + const expectedBody = []; + for (let i = 0; i < EVENT_BUFFER_LENGTH - 1; i++) { + expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` }); + } + + expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', { + body: expectedBody, + }); + }); + + test('should write buffered docs after buffer exceeded', async () => { + // write EVENT_BUFFER_LENGTH + 1 docs + for (let i = 0; i < EVENT_BUFFER_LENGTH + 1; i++) { + clusterClientAdapter.indexDocument({ body: { message: `foo ${i}` }, index: 'event-log' }); + } + + await retryUntil('cluster client bulk called', () => { + return clusterClient.callAsInternalUser.mock.calls.length >= 2; + }); + + const expectedBody = []; + for (let i = 0; i < EVENT_BUFFER_LENGTH; i++) { + expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` }); + } + + expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(1, 'bulk', { + body: expectedBody, + }); + + expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(2, 'bulk', { + body: [{ create: { _index: 'event-log' } }, { message: `foo 100` }], + }); + }); + + test('should handle lots of docs correctly with a delay in the bulk index', async () => { + // @ts-ignore + clusterClient.callAsInternalUser.mockImplementation = async () => await delay(100); + + const docs = times(EVENT_BUFFER_LENGTH * 10, (i) => ({ + body: { message: `foo ${i}` }, + index: 'event-log', + })); + + // write EVENT_BUFFER_LENGTH * 10 docs + for (const doc of docs) { + clusterClientAdapter.indexDocument(doc); + } + + await retryUntil('cluster client bulk called', () => { + return clusterClient.callAsInternalUser.mock.calls.length >= 10; + }); + + for (let i = 0; i < 10; i++) { + const expectedBody = []; + for (let j = 0; j < EVENT_BUFFER_LENGTH; j++) { + expectedBody.push( + { create: { _index: 'event-log' } }, + { message: `foo ${i * EVENT_BUFFER_LENGTH + j}` } + ); + } + + expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(i + 1, 'bulk', { + body: expectedBody, + }); + } }); }); @@ -575,3 +691,29 @@ describe('queryEventsBySavedObject', () => { `); }); }); + +type RetryableFunction = () => boolean; + +const RETRY_UNTIL_DEFAULT_COUNT = 20; +const RETRY_UNTIL_DEFAULT_WAIT = 1000; // milliseconds + +async function retryUntil( + label: string, + fn: RetryableFunction, + count: number = RETRY_UNTIL_DEFAULT_COUNT, + wait: number = RETRY_UNTIL_DEFAULT_WAIT +): Promise<boolean> { + while (count > 0) { + count--; + + if (fn()) return true; + + // eslint-disable-next-line no-console + console.log(`attempt failed waiting for "${label}", attempts left: ${count}`); + + if (count === 0) return false; + await delay(wait); + } + + return false; +} 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 fa9f9c36052a1..d1dcf621150a6 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 @@ -4,20 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Subject } from 'rxjs'; +import { bufferTime, filter, switchMap } from 'rxjs/operators'; import { reject, isUndefined } from 'lodash'; import { SearchResponse, Client } from 'elasticsearch'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, LegacyClusterClient } from 'src/core/server'; - -import { IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; +import { EsContext } from '.'; +import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; +export const EVENT_BUFFER_TIME = 1000; // milliseconds +export const EVENT_BUFFER_LENGTH = 100; + export type EsClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>; export type IClusterClientAdapter = PublicMethodsOf<ClusterClientAdapter>; +export interface Doc { + index: string; + body: IEvent; +} + export interface ConstructorOpts { logger: Logger; clusterClientPromise: Promise<EsClusterClient>; + context: EsContext; } export interface QueryEventsBySavedObjectResult { @@ -30,14 +41,67 @@ export interface QueryEventsBySavedObjectResult { export class ClusterClientAdapter { private readonly logger: Logger; private readonly clusterClientPromise: Promise<EsClusterClient>; + private readonly docBuffer$: Subject<Doc>; + private readonly context: EsContext; + private readonly docsBufferedFlushed: Promise<void>; constructor(opts: ConstructorOpts) { this.logger = opts.logger; this.clusterClientPromise = opts.clusterClientPromise; + this.context = opts.context; + this.docBuffer$ = new Subject<Doc>(); + + // buffer event log docs for time / buffer length, ignore empty + // buffers, then index the buffered docs; kick things off with a + // promise on the observable, which we'll wait on in shutdown + this.docsBufferedFlushed = this.docBuffer$ + .pipe( + bufferTime(EVENT_BUFFER_TIME, null, EVENT_BUFFER_LENGTH), + filter((docs) => docs.length > 0), + switchMap(async (docs) => await this.indexDocuments(docs)) + ) + .toPromise(); } - public async indexDocument(doc: unknown): Promise<void> { - await this.callEs<ReturnType<Client['index']>>('index', doc); + // This will be called at plugin stop() time; the assumption is any plugins + // depending on the event_log will already be stopped, and so will not be + // writing more event docs. We complete the docBuffer$ observable, + // and wait for the docsBufffered$ observable to complete via it's promise, + // and so should end up writing all events out that pass through, before + // Kibana shuts down (cleanly). + public async shutdown(): Promise<void> { + this.docBuffer$.complete(); + await this.docsBufferedFlushed; + } + + public indexDocument(doc: Doc): void { + this.docBuffer$.next(doc); + } + + async indexDocuments(docs: Doc[]): Promise<void> { + // If es initialization failed, don't try to index. + // Also, don't log here, we log the failure case in plugin startup + // instead, otherwise we'd be spamming the log (if done here) + if (!(await this.context.waitTillReady())) { + return; + } + + const bulkBody: Array<Record<string, unknown>> = []; + + for (const doc of docs) { + if (doc.body === undefined) continue; + + bulkBody.push({ create: { _index: doc.index } }); + bulkBody.push(doc.body); + } + + try { + await this.callEs<ReturnType<Client['bulk']>>('bulk', { body: bulkBody }); + } catch (err) { + this.logger.error( + `error writing bulk events: "${err.message}"; docs: ${JSON.stringify(bulkBody)}` + ); + } } public async doesIlmPolicyExist(policyName: string): Promise<boolean> { diff --git a/x-pack/plugins/event_log/server/es/context.mock.ts b/x-pack/plugins/event_log/server/es/context.mock.ts index aac7c684218aa..49a57fcb2b00d 100644 --- a/x-pack/plugins/event_log/server/es/context.mock.ts +++ b/x-pack/plugins/event_log/server/es/context.mock.ts @@ -18,6 +18,7 @@ const createContextMock = () => { logger: loggingSystemMock.createLogger(), esNames: namesMock.create(), initialize: jest.fn(), + shutdown: jest.fn(), waitTillReady: jest.fn(async () => true), esAdapter: clusterClientAdapterMock.create(), initialized: true, diff --git a/x-pack/plugins/event_log/server/es/context.ts b/x-pack/plugins/event_log/server/es/context.ts index 8c967e68299b5..d7f67620e7968 100644 --- a/x-pack/plugins/event_log/server/es/context.ts +++ b/x-pack/plugins/event_log/server/es/context.ts @@ -18,6 +18,7 @@ export interface EsContext { esNames: EsNames; esAdapter: IClusterClientAdapter; initialize(): void; + shutdown(): Promise<void>; waitTillReady(): Promise<boolean>; initialized: boolean; } @@ -52,6 +53,7 @@ class EsContextImpl implements EsContext { this.esAdapter = new ClusterClientAdapter({ logger: params.logger, clusterClientPromise: params.clusterClientPromise, + context: this, }); } @@ -74,6 +76,10 @@ class EsContextImpl implements EsContext { }); } + async shutdown() { + await this.esAdapter.shutdown(); + } + // waits till the ES initialization is done, returns true if it was successful, // false if it was not successful async waitTillReady(): Promise<boolean> { diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index ea699af45ccd2..28b4f5325dcb7 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -59,7 +59,8 @@ describe('EventLogger', () => { eventLogger.logEvent({}); await waitForLogEvent(systemLogger); delay(WRITE_LOG_WAIT_MILLIS); // sleep a bit longer since event logging is async - expect(esContext.esAdapter.indexDocument).not.toHaveBeenCalled(); + expect(esContext.esAdapter.indexDocument).toHaveBeenCalled(); + expect(esContext.esAdapter.indexDocuments).not.toHaveBeenCalled(); }); test('method logEvent() writes expected default values', async () => { diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index 658d90d809652..db24379bb46ba 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -20,14 +20,10 @@ import { EventSchema, } from './types'; import { SAVED_OBJECT_REL_PRIMARY } from './types'; +import { Doc } from './es/cluster_client_adapter'; type SystemLogger = Plugin['systemLogger']; -interface Doc { - index: string; - body: IEvent; -} - interface IEventLoggerCtorParams { esContext: EsContext; eventLogService: EventLogService; @@ -159,44 +155,9 @@ function validateEvent(eventLogService: IEventLogService, event: IEvent): IValid export const EVENT_LOGGED_PREFIX = `event logged: `; function logEventDoc(logger: Logger, doc: Doc): void { - setImmediate(() => { - logger.info(`${EVENT_LOGGED_PREFIX}${JSON.stringify(doc.body)}`); - }); + logger.info(`event logged: ${JSON.stringify(doc.body)}`); } function indexEventDoc(esContext: EsContext, doc: Doc): void { - // TODO: - // the setImmediate() on an async function is a little overkill, but, - // setImmediate() may be tweakable via node params, whereas async - // tweaking is in the v8 params realm, which is very dicey. - // Long-term, we should probably create an in-memory queue for this, so - // we can explictly see/set the queue lengths. - - // already verified this.clusterClient isn't null above - setImmediate(async () => { - try { - await indexLogEventDoc(esContext, doc); - } catch (err) { - esContext.logger.warn(`error writing event doc: ${err.message}`); - writeLogEventDocOnError(esContext, doc); - } - }); -} - -// whew, the thing that actually writes the event log document! -async function indexLogEventDoc(esContext: EsContext, doc: unknown) { - esContext.logger.debug(`writing to event log: ${JSON.stringify(doc)}`); - const success = await esContext.waitTillReady(); - if (!success) { - esContext.logger.debug(`event log did not initialize correctly, event not written`); - return; - } - - await esContext.esAdapter.indexDocument(doc); - esContext.logger.debug(`writing to event log complete`); -} - -// TODO: write log entry to a bounded queue buffer -function writeLogEventDocOnError(esContext: EsContext, doc: unknown) { - esContext.logger.warn(`unable to write event doc: ${JSON.stringify(doc)}`); + esContext.esAdapter.indexDocument(doc); } diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts deleted file mode 100644 index b30d83f24f261..0000000000000 --- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createBoundedQueue } from './bounded_queue'; -import { loggingSystemMock } from 'src/core/server/mocks'; - -const loggingService = loggingSystemMock.create(); -const logger = loggingService.get(); - -describe('basic', () => { - let discardedHelper: DiscardedHelper<number>; - let onDiscarded: (object: number) => void; - let queue2: ReturnType<typeof createBoundedQueue>; - let queue10: ReturnType<typeof createBoundedQueue>; - - beforeAll(() => { - discardedHelper = new DiscardedHelper(); - onDiscarded = discardedHelper.onDiscarded.bind(discardedHelper); - }); - - beforeEach(() => { - queue2 = createBoundedQueue<number>({ logger, maxLength: 2, onDiscarded }); - queue10 = createBoundedQueue<number>({ logger, maxLength: 10, onDiscarded }); - }); - - test('queued items: 0', () => { - discardedHelper.reset(); - expect(queue2.isEmpty()).toEqual(true); - expect(queue2.isFull()).toEqual(false); - expect(queue2.isCloseToFull()).toEqual(false); - expect(queue2.length).toEqual(0); - expect(queue2.maxLength).toEqual(2); - expect(queue2.pull(1)).toEqual([]); - expect(queue2.pull(100)).toEqual([]); - expect(discardedHelper.discarded).toEqual([]); - }); - - test('queued items: 1', () => { - discardedHelper.reset(); - queue2.push(1); - expect(queue2.isEmpty()).toEqual(false); - expect(queue2.isFull()).toEqual(false); - expect(queue2.isCloseToFull()).toEqual(false); - expect(queue2.length).toEqual(1); - expect(queue2.maxLength).toEqual(2); - expect(queue2.pull(1)).toEqual([1]); - expect(queue2.pull(1)).toEqual([]); - expect(discardedHelper.discarded).toEqual([]); - }); - - test('queued items: 2', () => { - discardedHelper.reset(); - queue2.push(1); - queue2.push(2); - expect(queue2.isEmpty()).toEqual(false); - expect(queue2.isFull()).toEqual(true); - expect(queue2.isCloseToFull()).toEqual(true); - expect(queue2.length).toEqual(2); - expect(queue2.maxLength).toEqual(2); - expect(queue2.pull(1)).toEqual([1]); - expect(queue2.pull(1)).toEqual([2]); - expect(queue2.pull(1)).toEqual([]); - expect(discardedHelper.discarded).toEqual([]); - }); - - test('queued items: 3', () => { - discardedHelper.reset(); - queue2.push(1); - queue2.push(2); - queue2.push(3); - expect(queue2.isEmpty()).toEqual(false); - expect(queue2.isFull()).toEqual(true); - expect(queue2.isCloseToFull()).toEqual(true); - expect(queue2.length).toEqual(2); - expect(queue2.maxLength).toEqual(2); - expect(queue2.pull(1)).toEqual([2]); - expect(queue2.pull(1)).toEqual([3]); - expect(queue2.pull(1)).toEqual([]); - expect(discardedHelper.discarded).toEqual([1]); - }); - - test('closeToFull()', () => { - discardedHelper.reset(); - - expect(queue10.isCloseToFull()).toEqual(false); - - for (let i = 1; i <= 8; i++) { - queue10.push(i); - expect(queue10.isCloseToFull()).toEqual(false); - } - - queue10.push(9); - expect(queue10.isCloseToFull()).toEqual(true); - - queue10.push(10); - expect(queue10.isCloseToFull()).toEqual(true); - - queue10.pull(2); - expect(queue10.isCloseToFull()).toEqual(false); - - queue10.push(11); - expect(queue10.isCloseToFull()).toEqual(true); - }); - - test('discarded', () => { - discardedHelper.reset(); - queue2.push(1); - queue2.push(2); - queue2.push(3); - expect(discardedHelper.discarded).toEqual([1]); - - discardedHelper.reset(); - queue2.push(4); - queue2.push(5); - expect(discardedHelper.discarded).toEqual([2, 3]); - }); - - test('pull', () => { - discardedHelper.reset(); - - expect(queue10.pull(4)).toEqual([]); - - for (let i = 1; i <= 10; i++) { - queue10.push(i); - } - - expect(queue10.pull(4)).toEqual([1, 2, 3, 4]); - expect(queue10.length).toEqual(6); - expect(queue10.pull(4)).toEqual([5, 6, 7, 8]); - expect(queue10.length).toEqual(2); - expect(queue10.pull(4)).toEqual([9, 10]); - expect(queue10.length).toEqual(0); - expect(queue10.pull(1)).toEqual([]); - expect(queue10.pull(4)).toEqual([]); - }); -}); - -class DiscardedHelper<T> { - private _discarded: T[]; - - constructor() { - this.reset(); - this._discarded = []; - this.onDiscarded = this.onDiscarded.bind(this); - } - - onDiscarded(object: T) { - this._discarded.push(object); - } - - public get discarded(): T[] { - return this._discarded; - } - - reset() { - this._discarded = []; - } -} diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.ts deleted file mode 100644 index 2c5ebcd38f5a8..0000000000000 --- a/x-pack/plugins/event_log/server/lib/bounded_queue.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Plugin } from '../plugin'; - -const CLOSE_TO_FULL_PERCENT = 0.9; - -type SystemLogger = Plugin['systemLogger']; - -export interface IBoundedQueue<T> { - maxLength: number; - length: number; - push(object: T): void; - pull(count: number): T[]; - isEmpty(): boolean; - isFull(): boolean; - isCloseToFull(): boolean; -} - -export interface CreateBoundedQueueParams<T> { - maxLength: number; - onDiscarded(object: T): void; - logger: SystemLogger; -} - -export function createBoundedQueue<T>(params: CreateBoundedQueueParams<T>): IBoundedQueue<T> { - if (params.maxLength <= 0) throw new Error(`invalid bounded queue maxLength ${params.maxLength}`); - - return new BoundedQueue<T>(params); -} - -class BoundedQueue<T> implements IBoundedQueue<T> { - private _maxLength: number; - private _buffer: T[]; - private _onDiscarded: (object: T) => void; - private _logger: SystemLogger; - - constructor(params: CreateBoundedQueueParams<T>) { - this._maxLength = params.maxLength; - this._buffer = []; - this._onDiscarded = params.onDiscarded; - this._logger = params.logger; - } - - public get maxLength(): number { - return this._maxLength; - } - - public get length(): number { - return this._buffer.length; - } - - isEmpty() { - return this._buffer.length === 0; - } - - isFull() { - return this._buffer.length >= this._maxLength; - } - - isCloseToFull() { - return this._buffer.length / this._maxLength >= CLOSE_TO_FULL_PERCENT; - } - - push(object: T) { - this.ensureRoom(); - this._buffer.push(object); - } - - pull(count: number) { - if (count <= 0) throw new Error(`invalid pull count ${count}`); - - return this._buffer.splice(0, count); - } - - private ensureRoom() { - if (this.length < this._maxLength) return; - - const discarded = this.pull(this.length - this._maxLength + 1); - for (const object of discarded) { - try { - this._onDiscarded(object!); - } catch (err) { - this._logger.warn(`error discarding circular buffer entry: ${err.message}`); - } - } - } -} diff --git a/x-pack/plugins/event_log/server/lib/ready_signal.ts b/x-pack/plugins/event_log/server/lib/ready_signal.ts index 58879649b83cb..706f3e79cc279 100644 --- a/x-pack/plugins/event_log/server/lib/ready_signal.ts +++ b/x-pack/plugins/event_log/server/lib/ready_signal.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface ReadySignal<T> { +export interface ReadySignal<T = void> { wait(): Promise<T>; signal(value: T): void; } diff --git a/x-pack/plugins/event_log/server/plugin.test.ts b/x-pack/plugins/event_log/server/plugin.test.ts new file mode 100644 index 0000000000000..e32bda9089701 --- /dev/null +++ b/x-pack/plugins/event_log/server/plugin.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, CoreStart } from 'src/core/server'; +import { coreMock } from 'src/core/server/mocks'; +import { IEventLogService } from './index'; +import { Plugin } from './plugin'; +import { spacesMock } from '../../spaces/server/mocks'; + +describe('event_log plugin', () => { + it('can setup and start', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const coreSetup = coreMock.createSetup() as CoreSetup<IEventLogService>; + const coreStart = coreMock.createStart() as CoreStart; + + const plugin = new Plugin(initializerContext); + const setup = await plugin.setup(coreSetup); + expect(typeof setup.getLogger).toBe('function'); + expect(typeof setup.getProviderActions).toBe('function'); + expect(typeof setup.isEnabled).toBe('function'); + expect(typeof setup.isIndexingEntries).toBe('function'); + expect(typeof setup.isLoggingEntries).toBe('function'); + expect(typeof setup.isProviderActionRegistered).toBe('function'); + expect(typeof setup.registerProviderActions).toBe('function'); + expect(typeof setup.registerSavedObjectProvider).toBe('function'); + + const spaces = spacesMock.createStart(); + const start = await plugin.start(coreStart, { spaces }); + expect(typeof start.getClient).toBe('function'); + }); + + it('can stop', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const mockLogger = initializerContext.logger.get(); + const coreSetup = coreMock.createSetup() as CoreSetup<IEventLogService>; + const coreStart = coreMock.createStart() as CoreStart; + + const plugin = new Plugin(initializerContext); + const spaces = spacesMock.createStart(); + await plugin.setup(coreSetup); + await plugin.start(coreStart, { spaces }); + await plugin.stop(); + expect(mockLogger.debug).toBeCalledWith('shutdown: waiting to finish'); + expect(mockLogger.debug).toBeCalledWith('shutdown: finished'); + }); +}); diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index f69850f166aee..d85de565b4d8e 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -115,6 +115,18 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi this.esContext.initialize(); } + // Log an error if initialiization didn't succeed. + // Note that waitTillReady() is used elsewhere as a gate to having the + // event log initialization complete - successfully or not. Other uses + // of this do not bother logging when success is false, as they are in + // paths that would cause log spamming. So we do it once, here, just to + // ensure an unsucccess initialization is logged when it occurs. + this.esContext.waitTillReady().then((success) => { + if (!success) { + this.systemLogger.error(`initialization failed, events will not be indexed`); + } + }); + // will log the event after initialization this.eventLogger.logEvent({ event: { action: ACTIONS.starting }, @@ -134,18 +146,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi return this.eventLogClientService; } - private createRouteHandlerContext = (): IContextProvider< - RequestHandler<unknown, unknown, unknown>, - 'eventLog' - > => { - return async (context, request) => { - return { - getEventLogClient: () => this.eventLogClientService!.getClient(request), - }; - }; - }; - - stop() { + async stop(): Promise<void> { this.systemLogger.debug('stopping plugin'); if (!this.eventLogger) throw new Error('eventLogger not initialized'); @@ -156,5 +157,20 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi event: { action: ACTIONS.stopping }, message: 'eventLog stopping', }); + + this.systemLogger.debug('shutdown: waiting to finish'); + await this.esContext?.shutdown(); + this.systemLogger.debug('shutdown: finished'); } + + private createRouteHandlerContext = (): IContextProvider< + RequestHandler<unknown, unknown, unknown>, + 'eventLog' + > => { + return async (context, request) => { + return { + getEventLogClient: () => this.eventLogClientService!.getClient(request), + }; + }; + }; } diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 303eeea6e510c..872b389d248a3 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -26,6 +26,7 @@ export type AgentActionType = | 'POLICY_CHANGE' | 'UNENROLL' | 'UPGRADE' + | 'SETTINGS' // INTERNAL* actions are mean to interupt long polling calls these actions will not be distributed to the agent | 'INTERNAL_POLICY_REASSIGN'; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5d79d41b7a631..7a6f6232b2d4f 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -252,12 +252,19 @@ export type PackageList = PackageListItem[]; export type PackageListItem = Installable<RegistrySearchResult>; export type PackagesGroupedByStatus = Record<ValueOf<InstallationStatus>, PackageList>; -export type PackageInfo = Installable< - // remove the properties we'll be altering/replacing from the base type - Omit<RegistryPackage, keyof PackageAdditions> & - // now add our replacement definitions - PackageAdditions ->; +export type PackageInfo = + | Installable< + // remove the properties we'll be altering/replacing from the base type + Omit<RegistryPackage, keyof PackageAdditions> & + // now add our replacement definitions + PackageAdditions + > + | Installable< + // remove the properties we'll be altering/replacing from the base type + Omit<ArchivePackage, keyof PackageAdditions> & + // now add our replacement definitions + PackageAdditions + >; export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index 81b56682b47e1..2fcbef75b9832 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -7,5 +7,5 @@ "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], "optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaReact", "esUiShared", "home"] + "requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils"] } diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx index a22e4e14055e3..9ebc8ea9380a9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx @@ -9,7 +9,7 @@ import { IFieldType } from 'src/plugins/data/public'; // @ts-ignore import { EuiSuggest, EuiSuggestItemProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useDebounce, useStartDeps } from '../hooks'; +import { useDebounce, useStartServices } from '../hooks'; import { INDEX_NAME, AGENT_SAVED_OBJECT_TYPE } from '../constants'; const DEBOUNCE_SEARCH_MS = 150; @@ -80,7 +80,7 @@ export const SearchBar: React.FunctionComponent<Props> = ({ ); }; -function transformSuggestionType(type: string): { iconType: string; color: string } { +export function transformSuggestionType(type: string): { iconType: string; color: string } { switch (type) { case 'field': return { iconType: 'kqlField', color: 'tint4' }; @@ -96,7 +96,7 @@ function transformSuggestionType(type: string): { iconType: string; color: strin } function useSuggestions(fieldPrefix: string, search: string) { - const { data } = useStartDeps(); + const { data } = useStartServices(); const debouncedSearch = useDebounce(search, DEBOUNCE_SEARCH_MS); const [suggestions, setSuggestions] = useState<Suggestion[]>([]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx index 80ecaa2493278..639a3e41b39fa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx @@ -25,7 +25,13 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; import { safeLoad } from 'js-yaml'; -import { useComboInput, useCore, useGetSettings, useInput, sendPutSettings } from '../hooks'; +import { + useComboInput, + useStartServices, + useGetSettings, + useInput, + sendPutSettings, +} from '../hooks'; import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs'; import { isDiffPathProtocol } from '../../../../common/'; @@ -37,7 +43,7 @@ interface Props { function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { const [isLoading, setIsloading] = React.useState(false); - const { notifications } = useCore(); + const { notifications } = useStartServices(); const kibanaUrlsInput = useComboInput([], (value) => { if (value.length === 0) { return [ diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index 9963753651671..ecd4227a54b65 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -51,8 +51,7 @@ export const PAGE_ROUTING_PATHS = { fleet: '/fleet', fleet_agent_list: '/fleet/agents', fleet_agent_details: '/fleet/agents/:agentId/:tabId?', - fleet_agent_details_events: '/fleet/agents/:agentId', - fleet_agent_details_details: '/fleet/agents/:agentId/details', + fleet_agent_details_logs: '/fleet/agents/:agentId/logs', fleet_enrollment_tokens: '/fleet/enrollment-tokens', data_streams: '/data-streams', }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/index.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/index.ts index 29843f6a3e5b1..6026a5579f65b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/index.ts @@ -5,10 +5,9 @@ */ export { useCapabilities } from './use_capabilities'; -export { useCore } from './use_core'; +export { useStartServices } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useKibanaVersion, KibanaVersionContext } from './use_kibana_version'; -export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; export { licenseService, useLicense } from './use_license'; export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index ed38e1a5ce4a1..40654645ecd3f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb } from 'src/core/public'; import { BASE_PATH, Page, DynamicPagePathValues, pagePathGetters } from '../constants'; -import { useCore } from './use_core'; +import { useStartServices } from './use_core'; const BASE_BREADCRUMB: ChromeBreadcrumb = { href: pagePathGetters.overview(), @@ -204,7 +204,7 @@ const breadcrumbGetters: { }; export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { - const { chrome, http } = useCore(); + const { chrome, http } = useStartServices(); const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map((breadcrumb) => ({ ...breadcrumb, href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts index d8535183bb84e..da5be82049c8e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCore } from './'; +import { useStartServices } from './'; export function useCapabilities() { - const core = useCore(); + const core = useStartServices(); return core.application.capabilities.fleet; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_core.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_core.ts index dad2eaa1d8e0f..f425831f6d6bc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_core.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_core.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'kibana/public'; +import { FleetStartServices } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -export function useCore(): CoreStart { - const { services } = useKibana<CoreStart>(); +export function useStartServices(): FleetStartServices { + const { services } = useKibana<FleetStartServices>(); if (services === null) { throw new Error('KibanaContextProvider not initialized'); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts deleted file mode 100644 index bf8f33297882e..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useContext } from 'react'; -import { FleetSetupDeps, FleetStartDeps } from '../../../plugin'; - -export const DepsContext = React.createContext<{ - setup: FleetSetupDeps; - start: FleetStartDeps; -} | null>(null); - -export function useSetupDeps() { - const deps = useContext(DepsContext); - if (deps === null) { - throw new Error('DepsContext not initialized'); - } - return deps.setup; -} - -export function useStartDeps() { - const deps = useContext(DepsContext); - if (deps === null) { - throw new Error('StartDepsContext not initialized'); - } - return deps.start; -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_kibana_link.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_kibana_link.ts index 58537b2075c16..5faa3bfcab4af 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_kibana_link.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_kibana_link.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCore } from './'; +import { useStartServices } from './'; const KIBANA_BASE_PATH = '/app/kibana'; export function useKibanaLink(path: string = '/') { - const core = useCore(); + const core = useStartServices(); return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_link.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_link.ts index 1b17c5cb0b1f3..40c0689905932 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_link.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_link.ts @@ -11,14 +11,14 @@ import { DynamicPagePathValues, pagePathGetters, } from '../constants'; -import { useCore } from './'; +import { useStartServices } from './'; const getPath = (page: StaticPage | DynamicPage, values: DynamicPagePathValues = {}): string => { return values ? pagePathGetters[page](values) : pagePathGetters[page as StaticPage](); }; export const useLink = () => { - const core = useCore(); + const core = useStartServices(); return { getPath, getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index 51c897b3661cc..61a5f1eabc2af 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -14,16 +14,15 @@ import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode } from '@elastic/eu import { CoreStart, AppMountParameters } from 'src/core/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../xpack_legacy/common'; -import { FleetSetupDeps, FleetConfigType, FleetStartDeps } from '../../plugin'; +import { FleetConfigType, FleetStartServices } from '../../plugin'; import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentPolicyApp, FleetApp, DataStreamApp } from './sections'; import { - DepsContext, ConfigContext, useConfig, - useCore, + useStartServices, sendSetup, sendGetPermissionsCheck, licenseService, @@ -67,7 +66,7 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep useBreadcrumbs('base'); const { agents } = useConfig(); - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false); const [permissionsError, setPermissionsError] = useState<string>(); @@ -227,48 +226,40 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep const IngestManagerApp = ({ basepath, - coreStart, - setupDeps, - startDeps, + startServices, config, history, kibanaVersion, extensions, }: { basepath: string; - coreStart: CoreStart; - setupDeps: FleetSetupDeps; - startDeps: FleetStartDeps; + startServices: FleetStartServices; config: FleetConfigType; history: AppMountParameters['history']; kibanaVersion: string; extensions: UIExtensionsStorage; }) => { - const isDarkMode = useObservable<boolean>(coreStart.uiSettings.get$('theme:darkMode')); + const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode')); return ( - <coreStart.i18n.Context> - <KibanaContextProvider services={{ ...coreStart }}> - <DepsContext.Provider value={{ setup: setupDeps, start: startDeps }}> - <ConfigContext.Provider value={config}> - <KibanaVersionContext.Provider value={kibanaVersion}> - <EuiThemeProvider darkMode={isDarkMode}> - <UIExtensionsContext.Provider value={extensions}> - <IngestManagerRoutes history={history} basepath={basepath} /> - </UIExtensionsContext.Provider> - </EuiThemeProvider> - </KibanaVersionContext.Provider> - </ConfigContext.Provider> - </DepsContext.Provider> + <startServices.i18n.Context> + <KibanaContextProvider services={{ ...startServices }}> + <ConfigContext.Provider value={config}> + <KibanaVersionContext.Provider value={kibanaVersion}> + <EuiThemeProvider darkMode={isDarkMode}> + <UIExtensionsContext.Provider value={extensions}> + <IngestManagerRoutes history={history} basepath={basepath} /> + </UIExtensionsContext.Provider> + </EuiThemeProvider> + </KibanaVersionContext.Provider> + </ConfigContext.Provider> </KibanaContextProvider> - </coreStart.i18n.Context> + </startServices.i18n.Context> ); }; export function renderApp( - coreStart: CoreStart, + startServices: FleetStartServices, { element, appBasePath, history }: AppMountParameters, - setupDeps: FleetSetupDeps, - startDeps: FleetStartDeps, config: FleetConfigType, kibanaVersion: string, extensions: UIExtensionsStorage @@ -276,9 +267,7 @@ export function renderApp( ReactDOM.render( <IngestManagerApp basepath={appBasePath} - coreStart={coreStart} - setupDeps={setupDeps} - startDeps={startDeps} + startServices={startServices} config={config} history={history} kibanaVersion={kibanaVersion} diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx index 376de7e2e6a07..93bfe489a1bf4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx @@ -26,6 +26,12 @@ const Container = styled.div` flex-direction: column; `; +const Wrapper = styled.div` + display: flex; + flex-direction: column; + flex: 1; +`; + const Nav = styled.nav` background: ${(props) => props.theme.eui.euiColorEmptyShade}; border-bottom: ${(props) => props.theme.eui.euiBorderThin}; @@ -56,7 +62,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({ /> )} <Container> - <div> + <Wrapper> <Nav> <EuiFlexGroup gutterSize="l" alignItems="center"> <EuiFlexItem> @@ -126,7 +132,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({ </EuiFlexGroup> </Nav> {children} - </div> + </Wrapper> <AlphaMessaging /> </Container> </> diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx index 03efe20f96a51..e49ef152f8306 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ export { DefaultLayout } from './default'; -export { WithHeaderLayout } from './with_header'; +export { WithHeaderLayout, WithHeaderLayoutProps } from './with_header'; export { WithoutHeaderLayout } from './without_header'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/with_header.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/with_header.tsx index 4b21a15a73645..bca0e2889483f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/with_header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/with_header.tsx @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import styled from 'styled-components'; -import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; +import { EuiPageBody, EuiSpacer } from '@elastic/eui'; import { Header, HeaderProps } from '../components'; - -const Page = styled(EuiPage)` - background: ${(props) => props.theme.eui.euiColorEmptyShade}; -`; +import { Page, ContentWrapper } from './without_header'; export interface WithHeaderLayoutProps extends HeaderProps { restrictWidth?: number; @@ -37,8 +33,10 @@ export const WithHeaderLayout: React.FC<WithHeaderLayoutProps> = ({ data-test-subj={dataTestSubj ? `${dataTestSubj}_page` : undefined} > <EuiPageBody> - <EuiSpacer size="m" /> - {children} + <ContentWrapper> + <EuiSpacer size="m" /> + {children} + </ContentWrapper> </EuiPageBody> </Page> </Fragment> diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/without_header.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/without_header.tsx index 08f6244242a3d..93ad997780015 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/without_header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/without_header.tsx @@ -7,8 +7,17 @@ import React, { Fragment } from 'react'; import styled from 'styled-components'; import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; -const Page = styled(EuiPage)` +export const Page = styled(EuiPage)` background: ${(props) => props.theme.eui.euiColorEmptyShade}; + width: 100%; + align-self: center; + margin-left: 0; + margin-right: 0; + flex: 1; +`; + +export const ContentWrapper = styled.div` + height: 100%; `; interface Props { @@ -20,8 +29,10 @@ export const WithoutHeaderLayout: React.FC<Props> = ({ restrictWidth, children } <Fragment> <Page restrictWidth={restrictWidth || 1200}> <EuiPageBody> - <EuiSpacer size="m" /> - {children} + <ContentWrapper> + <EuiSpacer size="m" /> + {children} + </ContentWrapper> </EuiPageBody> </Page> </Fragment> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx index 41201f9612f13..9e2a7ae8f8f47 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -9,7 +9,7 @@ import { EuiConfirmModal, EuiOverlayMask, EuiFormRow, EuiFieldText } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../types'; -import { sendCopyAgentPolicy, useCore } from '../../../hooks'; +import { sendCopyAgentPolicy, useStartServices } from '../../../hooks'; interface Props { children: (copyAgentPolicy: CopyAgentPolicy) => React.ReactElement; @@ -20,7 +20,7 @@ export type CopyAgentPolicy = (agentPolicy: AgentPolicy, onSuccess?: OnSuccessCa type OnSuccessCallback = (newAgentPolicy: AgentPolicy) => void; export const AgentPolicyCopyProvider: React.FunctionComponent<Props> = ({ children }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [agentPolicy, setAgentPolicy] = useState<AgentPolicy>(); const [newAgentPolicy, setNewAgentPolicy] = useState<Pick<AgentPolicy, 'name' | 'description'>>(); const [isModalOpen, setIsModalOpen] = useState<boolean>(false); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx index 41704f69958a0..7afb028dded2a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -9,7 +9,7 @@ import { EuiConfirmModal, EuiOverlayMask, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; -import { sendDeleteAgentPolicy, useCore, useConfig, sendRequest } from '../../../hooks'; +import { sendDeleteAgentPolicy, useStartServices, useConfig, sendRequest } from '../../../hooks'; interface Props { children: (deleteAgentPolicy: DeleteAgentPolicy) => React.ReactElement; @@ -20,7 +20,7 @@ export type DeleteAgentPolicy = (agentPolicy: string, onSuccess?: OnSuccessCallb type OnSuccessCallback = (agentPolicyDeleted: string) => void; export const AgentPolicyDeleteProvider: React.FunctionComponent<Props> = ({ children }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index 773d53484147a..7b0075e160c47 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -20,7 +20,7 @@ import { EuiButton, EuiCallOut, } from '@elastic/eui'; -import { useGetOneAgentPolicyFull, useGetOneAgentPolicy, useCore } from '../../../hooks'; +import { useGetOneAgentPolicyFull, useGetOneAgentPolicy, useStartServices } from '../../../hooks'; import { Loading } from '../../../components'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../services'; @@ -32,7 +32,7 @@ const FlyoutBody = styled(EuiFlyoutBody)` export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => void }>( ({ policyId, onClose }) => { - const core = useCore(); + const core = useStartServices(); const { isLoading: isLoadingYaml, data: yamlData, error } = useGetOneAgentPolicyFull(policyId); const { data: agentPolicyData } = useGetOneAgentPolicy(policyId); const body = isLoadingYaml ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx index 8de40edc40331..e86ac9e3bd03c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx @@ -8,7 +8,7 @@ import React, { Fragment, useMemo, useRef, useState } from 'react'; import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useCore, sendRequest, sendDeletePackagePolicy, useConfig } from '../../../hooks'; +import { useStartServices, sendRequest, sendDeletePackagePolicy, useConfig } from '../../../hooks'; import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { AgentPolicy } from '../../../types'; @@ -28,7 +28,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent<Props> = ({ agentPolicy, children, }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index a837ed33e4110..62792b84105ab 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -28,7 +28,7 @@ import { useLink, useBreadcrumbs, sendCreatePackagePolicy, - useCore, + useStartServices, useConfig, sendGetAgentStatus, } from '../../../hooks'; @@ -60,7 +60,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const { notifications, application: { navigateToApp }, - } = useCore(); + } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index fe3955c84dec3..b33976d53fe95 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../../../types'; import { useLink, - useCore, + useStartServices, useCapabilities, sendUpdateAgentPolicy, useConfig, @@ -33,7 +33,7 @@ const FormWrapper = styled.div` export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( ({ agentPolicy: originalAgentPolicy }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 7528c923f0abd..0099fb3c84d12 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -26,7 +26,7 @@ import { useGetOneAgentPolicy, useLink, useBreadcrumbs, - useCore, + useStartServices, useFleetStatus, } from '../../../hooks'; import { Loading, Error } from '../../../components'; @@ -56,7 +56,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { const { refreshAgentStatus } = agentStatusRequest; const { application: { navigateToApp }, - } = useCore(); + } = useStartServices(); const routeState = useIntraAppState<AgentPolicyDetailsDeployAgentAction>(); const agentStatus = agentStatusRequest.data?.results; const queryParams = new URLSearchParams(useLocation().search); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index bfc10848d378f..c0db51873e52e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -19,7 +19,7 @@ import { AgentPolicy, PackageInfo, UpdatePackagePolicy } from '../../../types'; import { useLink, useBreadcrumbs, - useCore, + useStartServices, useConfig, sendUpdatePackagePolicy, sendGetAgentStatus, @@ -47,7 +47,7 @@ import { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest import { PackagePolicyEditExtensionComponentProps } from '../../../types'; export const EditPackagePolicyPage: React.FunctionComponent = () => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx index f10f36174fe82..364df44a59e18 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import { dataTypes } from '../../../../../../../common'; import { NewAgentPolicy, AgentPolicy } from '../../../../types'; -import { useCapabilities, useCore, sendCreateAgentPolicy } from '../../../../hooks'; +import { useCapabilities, useStartServices, sendCreateAgentPolicy } from '../../../../hooks'; import { AgentPolicyForm, agentPolicyFormValidation } from '../../components'; const FlyoutWithHigherZIndex = styled(EuiFlyout)` @@ -38,7 +38,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({ onClose, ...restOfProps }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const hasWriteCapabilites = useCapabilities().write; const [agentPolicy, setAgentPolicy] = useState<NewAgentPolicy>({ name: '', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_events_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_events_table.tsx deleted file mode 100644 index c1a1b3862728d..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_events_table.tsx +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { - EuiBasicTable, - // @ts-ignore - EuiSuggest, - EuiFlexGroup, - EuiButton, - EuiSpacer, - EuiFlexItem, - EuiBadge, - EuiText, - EuiButtonIcon, - EuiCodeBlock, -} from '@elastic/eui'; -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, FormattedTime } from '@kbn/i18n/react'; -import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../../constants'; -import { Agent, AgentEvent } from '../../../../types'; -import { usePagination, useGetOneAgentEvents } from '../../../../hooks'; -import { SearchBar } from '../../../../components/search_bar'; -import { TYPE_LABEL, SUBTYPE_LABEL } from './type_labels'; - -function useSearch() { - const [state, setState] = useState<{ search: string }>({ - search: '', - }); - - const setSearch = (s: string) => - setState({ - search: s, - }); - - return { - ...state, - setSearch, - }; -} - -export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ agent }) => { - const { pageSizeOptions, pagination, setPagination } = usePagination(); - const { search, setSearch } = useSearch(); - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ - [key: string]: JSX.Element; - }>({}); - - const { isLoading, data, resendRequest } = useGetOneAgentEvents(agent.id, { - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: search && search.trim() !== '' ? search.trim() : undefined, - }); - - const refresh = () => resendRequest(); - - const total = data ? data.total : 0; - const list = data ? data.list : []; - const paginationOptions = { - pageIndex: pagination.currentPage - 1, - pageSize: pagination.pageSize, - totalItemCount: total, - pageSizeOptions, - }; - - const toggleDetails = (agentEvent: AgentEvent) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMapValues[agentEvent.id]) { - delete itemIdToExpandedRowMapValues[agentEvent.id]; - } else { - const details = ( - <div style={{ width: '100%' }}> - <div> - <EuiText size="s"> - <strong> - <FormattedMessage - id="xpack.fleet.agentEventsList.messageDetailsTitle" - defaultMessage="Message" - /> - </strong> - <EuiSpacer size="xs" /> - <p>{agentEvent.message}</p> - </EuiText> - </div> - {agentEvent.payload ? ( - <div> - <EuiSpacer size="s" /> - <EuiText size="s"> - <strong> - <FormattedMessage - id="xpack.fleet.agentEventsList.payloadDetailsTitle" - defaultMessage="Payload" - /> - </strong> - </EuiText> - <EuiSpacer size="xs" /> - <EuiCodeBlock language="json" paddingSize="s" overflowHeight={200}> - {JSON.stringify(agentEvent.payload, null, 2)} - </EuiCodeBlock> - </div> - ) : null} - </div> - ); - itemIdToExpandedRowMapValues[agentEvent.id] = details; - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; - - const columns = [ - { - field: 'timestamp', - name: i18n.translate('xpack.fleet.agentEventsList.timestampColumnTitle', { - defaultMessage: 'Timestamp', - }), - render: (timestamp: string) => ( - <FormattedTime - value={new Date(timestamp)} - month="short" - day="numeric" - year="numeric" - hour="numeric" - minute="numeric" - second="numeric" - /> - ), - sortable: true, - width: '18%', - }, - { - field: 'type', - name: i18n.translate('xpack.fleet.agentEventsList.typeColumnTitle', { - defaultMessage: 'Type', - }), - width: '10%', - render: (type: AgentEvent['type']) => - TYPE_LABEL[type] || <EuiBadge color="hollow">{type}</EuiBadge>, - }, - { - field: 'subtype', - name: i18n.translate('xpack.fleet.agentEventsList.subtypeColumnTitle', { - defaultMessage: 'Subtype', - }), - width: '13%', - render: (subtype: AgentEvent['subtype']) => - SUBTYPE_LABEL[subtype] || <EuiBadge color="hollow">{subtype}</EuiBadge>, - }, - { - field: 'message', - name: i18n.translate('xpack.fleet.agentEventsList.messageColumnTitle', { - defaultMessage: 'Message', - }), - render: (value: string) => ( - <EuiText size="xs" className="eui-textTruncate"> - {value} - </EuiText> - ), - }, - { - align: RIGHT_ALIGNMENT, - width: '40px', - isExpander: true, - render: (agentEvent: AgentEvent) => ( - <EuiButtonIcon - onClick={() => toggleDetails(agentEvent)} - aria-label={ - itemIdToExpandedRowMap[agentEvent.id] - ? i18n.translate('xpack.fleet.agentEventsList.collapseDetailsAriaLabel', { - defaultMessage: 'Hide details', - }) - : i18n.translate('xpack.fleet.agentEventsList.expandDetailsAriaLabel', { - defaultMessage: 'Show details', - }) - } - iconType={itemIdToExpandedRowMap[agentEvent.id] ? 'arrowUp' : 'arrowDown'} - /> - ), - }, - ]; - - const onClickRefresh = () => { - refresh(); - }; - - const onChange = ({ page }: { page: { index: number; size: number } }) => { - const newPagination = { - ...pagination, - currentPage: page.index + 1, - pageSize: page.size, - }; - - setPagination(newPagination); - }; - - return ( - <> - <EuiFlexGroup> - <EuiFlexItem> - <SearchBar - value={search} - onChange={setSearch} - fieldPrefix={AGENT_EVENT_SAVED_OBJECT_TYPE} - placeholder={i18n.translate('xpack.fleet.agentEventsList.searchPlaceholderText', { - defaultMessage: 'Search for activity logs', - })} - /> - </EuiFlexItem> - <EuiFlexItem grow={null}> - <EuiButton iconType="refresh" onClick={onClickRefresh}> - <FormattedMessage - id="xpack.fleet.agentEventsList.refreshButton" - defaultMessage="Refresh" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="m" /> - <EuiBasicTable<AgentEvent> - onChange={onChange} - items={list} - itemId="id" - columns={columns} - pagination={paginationOptions} - loading={isLoading} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} - /> - </> - ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/build_query.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/build_query.test.ts new file mode 100644 index 0000000000000..610c2feacf99e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/build_query.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildQuery } from './build_query'; + +describe('Fleet - buildQuery', () => { + it('should work', () => { + expect( + buildQuery({ agentId: 'some-agent-id', datasets: [], logLevels: [], userQuery: '' }) + ).toEqual( + 'elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.*)' + ); + + expect( + buildQuery({ + agentId: 'some-agent-id', + datasets: ['elastic_agent'], + logLevels: [], + userQuery: '', + }) + ).toEqual('elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent)'); + + expect( + buildQuery({ + agentId: 'some-agent-id', + datasets: ['elastic_agent', 'elastic_agent.filebeat'], + logLevels: ['error'], + userQuery: '', + }) + ).toEqual( + 'elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.filebeat) and (log.level:error)' + ); + + expect( + buildQuery({ + agentId: 'some-agent-id', + datasets: [], + logLevels: ['error', 'info', 'warn'], + userQuery: '', + }) + ).toEqual( + 'elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.*) and (log.level:error or log.level:info or log.level:warn)' + ); + + expect( + buildQuery({ + agentId: 'some-agent-id', + datasets: ['elastic_agent'], + logLevels: ['error', 'info', 'warn'], + userQuery: 'FLEET_GATEWAY and input.type:*', + }) + ).toEqual( + '(elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent) and (log.level:error or log.level:info or log.level:warn)) and (FLEET_GATEWAY and input.type:*)' + ); + + expect( + buildQuery({ + agentId: 'some-agent-id', + datasets: [], + logLevels: [], + userQuery: 'FLEET_GATEWAY and input.type:*', + }) + ).toEqual( + '(elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.*)) and (FLEET_GATEWAY and input.type:*)' + ); + + expect( + buildQuery({ + agentId: 'some-agent-id', + datasets: [], + logLevels: ['error'], + userQuery: 'FLEET_GATEWAY and input.type:*', + }) + ).toEqual( + '(elastic_agent.id:some-agent-id and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.*) and (log.level:error)) and (FLEET_GATEWAY and input.type:*)' + ); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/build_query.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/build_query.ts new file mode 100644 index 0000000000000..39d383cad503d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/build_query.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + DATASET_FIELD, + AGENT_DATASET, + AGENT_DATASET_PATTERN, + LOG_LEVEL_FIELD, + AGENT_ID_FIELD, +} from './constants'; + +export const buildQuery = ({ + agentId, + datasets, + logLevels, + userQuery, +}: { + agentId: string; + datasets: string[]; + logLevels: string[]; + userQuery: string; +}): string => { + // Filter on agent ID + const agentIdQuery = `${AGENT_ID_FIELD.name}:${agentId}`; + + // Filter on selected datasets if given, fall back to filtering on dataset: elastic_agent|elastic_agent.* + const datasetQuery = datasets.length + ? datasets.map((dataset) => `${DATASET_FIELD.name}:${dataset}`).join(' or ') + : `${DATASET_FIELD.name}:${AGENT_DATASET} or ${DATASET_FIELD.name}:${AGENT_DATASET_PATTERN}`; + + // Filter on log levels + const logLevelQuery = logLevels.map((level) => `${LOG_LEVEL_FIELD.name}:${level}`).join(' or '); + + // Agent ID + datasets query + const agentQuery = `${agentIdQuery} and (${datasetQuery})`; + + // Agent ID + datasets + log levels query + const baseQuery = logLevelQuery ? `${agentQuery} and (${logLevelQuery})` : agentQuery; + + // Agent ID + datasets + log levels + user input query + const finalQuery = userQuery ? `(${baseQuery}) and (${userQuery})` : baseQuery; + + return finalQuery; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx new file mode 100644 index 0000000000000..b56e27356ef34 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const AGENT_LOG_INDEX_PATTERN = 'logs-elastic_agent-*,logs-elastic_agent.*-*'; +export const AGENT_DATASET = 'elastic_agent'; +export const AGENT_DATASET_PATTERN = 'elastic_agent.*'; +export const AGENT_ID_FIELD = { + name: 'elastic_agent.id', + type: 'string', +}; +export const DATASET_FIELD = { + name: 'data_stream.dataset', + type: 'string', + aggregatable: true, +}; +export const LOG_LEVEL_FIELD = { + name: 'log.level', + type: 'string', + aggregatable: true, +}; +export const DEFAULT_DATE_RANGE = { + start: 'now-1d', + end: 'now', +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx new file mode 100644 index 0000000000000..bc3cfd84d2379 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo, useState, useEffect } from 'react'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useStartServices } from '../../../../../hooks'; +import { AGENT_LOG_INDEX_PATTERN, DATASET_FIELD, AGENT_DATASET } from './constants'; + +export const DatasetFilter: React.FunctionComponent<{ + selectedDatasets: string[]; + onToggleDataset: (dataset: string) => void; +}> = memo(({ selectedDatasets, onToggleDataset }) => { + const { data } = useStartServices(); + const [isOpen, setIsOpen] = useState<boolean>(false); + const [isLoading, setIsLoading] = useState<boolean>(false); + const [datasetValues, setDatasetValues] = useState<string[]>([AGENT_DATASET]); + + useEffect(() => { + const fetchValues = async () => { + setIsLoading(true); + try { + const values = await data.autocomplete.getValueSuggestions({ + indexPattern: { + title: AGENT_LOG_INDEX_PATTERN, + fields: [DATASET_FIELD], + }, + field: DATASET_FIELD, + query: '', + }); + setDatasetValues(values.sort()); + } catch (e) { + setDatasetValues([AGENT_DATASET]); + } + setIsLoading(false); + }; + fetchValues(); + }, [data.autocomplete]); + + return ( + <EuiPopover + button={ + <EuiFilterButton + iconType="arrowDown" + onClick={() => setIsOpen(true)} + isSelected={isOpen} + isLoading={isLoading} + numFilters={datasetValues.length} + hasActiveFilters={selectedDatasets.length > 0} + numActiveFilters={selectedDatasets.length} + > + {i18n.translate('xpack.fleet.agentLogs.datasetSelectText', { + defaultMessage: 'Dataset', + })} + </EuiFilterButton> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + panelPaddingSize="none" + > + {datasetValues.map((dataset) => ( + <EuiFilterSelectItem + checked={selectedDatasets.includes(dataset) ? 'on' : undefined} + key={dataset} + onClick={() => onToggleDataset(dataset)} + > + {dataset} + </EuiFilterSelectItem> + ))} + </EuiPopover> + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx new file mode 100644 index 0000000000000..b034168dc8a15 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo, useState, useEffect } from 'react'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useStartServices } from '../../../../../hooks'; +import { AGENT_LOG_INDEX_PATTERN, LOG_LEVEL_FIELD } from './constants'; + +export const LogLevelFilter: React.FunctionComponent<{ + selectedLevels: string[]; + onToggleLevel: (level: string) => void; +}> = memo(({ selectedLevels, onToggleLevel }) => { + const { data } = useStartServices(); + const [isOpen, setIsOpen] = useState<boolean>(false); + const [isLoading, setIsLoading] = useState<boolean>(false); + const [levelValues, setLevelValues] = useState<string[]>([]); + + useEffect(() => { + const fetchValues = async () => { + setIsLoading(true); + try { + const values = await data.autocomplete.getValueSuggestions({ + indexPattern: { + title: AGENT_LOG_INDEX_PATTERN, + fields: [LOG_LEVEL_FIELD], + }, + field: LOG_LEVEL_FIELD, + query: '', + }); + setLevelValues(values.sort()); + } catch (e) { + setLevelValues([]); + } + setIsLoading(false); + }; + fetchValues(); + }, [data.autocomplete]); + + return ( + <EuiPopover + button={ + <EuiFilterButton + iconType="arrowDown" + onClick={() => setIsOpen(true)} + isSelected={isOpen} + isLoading={isLoading} + numFilters={levelValues.length} + hasActiveFilters={selectedLevels.length > 0} + numActiveFilters={selectedLevels.length} + > + {i18n.translate('xpack.fleet.agentLogs.logLevelSelectText', { + defaultMessage: 'Log level', + })} + </EuiFilterButton> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + panelPaddingSize="none" + > + {levelValues.map((level) => ( + <EuiFilterSelectItem + checked={selectedLevels.includes(level) ? 'on' : undefined} + key={level} + onClick={() => onToggleLevel(level)} + > + {level} + </EuiFilterSelectItem> + ))} + </EuiPopover> + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx new file mode 100644 index 0000000000000..e033781a850a0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo, useMemo, useState, useCallback } from 'react'; +import styled from 'styled-components'; +import url from 'url'; +import { encode } from 'rison-node'; +import { stringify } from 'query-string'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiFilterGroup, + EuiPanel, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RedirectAppLinks } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { TimeRange, esKuery } from '../../../../../../../../../../../src/plugins/data/public'; +import { LogStream } from '../../../../../../../../../infra/public'; +import { Agent } from '../../../../../types'; +import { useStartServices } from '../../../../../hooks'; +import { AGENT_DATASET, DEFAULT_DATE_RANGE } from './constants'; +import { DatasetFilter } from './filter_dataset'; +import { LogLevelFilter } from './filter_log_level'; +import { LogQueryBar } from './query_bar'; +import { buildQuery } from './build_query'; + +const WrapperFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const DatePickerFlexItem = styled(EuiFlexItem)` + max-width: 312px; +`; + +export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agent }) => { + const { data, application, http } = useStartServices(); + + // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) + const getDateRangeTimestamps = useCallback( + (timeRange: TimeRange) => { + const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); + return min && max + ? { + startTimestamp: min.valueOf(), + endTimestamp: max.valueOf(), + } + : undefined; + }, + [data.query.timefilter.timefilter] + ); + + // Initial time range filter + const [dateRange, setDateRange] = useState<{ + startExpression: string; + endExpression: string; + startTimestamp: number; + endTimestamp: number; + }>({ + startExpression: DEFAULT_DATE_RANGE.start, + endExpression: DEFAULT_DATE_RANGE.end, + ...getDateRangeTimestamps({ from: DEFAULT_DATE_RANGE.start, to: DEFAULT_DATE_RANGE.end })!, + }); + + const tryUpdateDateRange = useCallback( + (timeRange: TimeRange) => { + const timestamps = getDateRangeTimestamps(timeRange); + if (timestamps) { + setDateRange({ + startExpression: timeRange.from, + endExpression: timeRange.to, + ...timestamps, + }); + } + }, + [getDateRangeTimestamps] + ); + + // Filters + const [selectedLogLevels, setSelectedLogLevels] = useState<string[]>([]); + const [selectedDatasets, setSelectedDatasets] = useState<string[]>([AGENT_DATASET]); + + // User query state + const [query, setQuery] = useState<string>(''); + const [draftQuery, setDraftQuery] = useState<string>(''); + const [isDraftQueryValid, setIsDraftQueryValid] = useState<boolean>(true); + const onUpdateDraftQuery = useCallback((newDraftQuery: string, runQuery?: boolean) => { + setDraftQuery(newDraftQuery); + try { + esKuery.fromKueryExpression(newDraftQuery); + setIsDraftQueryValid(true); + if (runQuery) { + setQuery(newDraftQuery); + } + } catch (err) { + setIsDraftQueryValid(false); + } + }, []); + + // Build final log stream query from agent id, datasets, log levels, and user input + const logStreamQuery = useMemo( + () => + buildQuery({ + agentId: agent.id, + datasets: selectedDatasets, + logLevels: selectedLogLevels, + userQuery: query, + }), + [agent.id, query, selectedDatasets, selectedLogLevels] + ); + + // Generate URL to pass page state to Logs UI + const viewInLogsUrl = useMemo( + () => + http.basePath.prepend( + url.format({ + pathname: '/app/logs/stream', + search: stringify( + { + logPosition: encode({ + start: dateRange.startExpression, + end: dateRange.endExpression, + streamLive: false, + }), + logFilter: encode({ + expression: logStreamQuery, + kind: 'kuery', + }), + }, + { sort: false, encode: false } + ), + }) + ), + [logStreamQuery, dateRange.endExpression, dateRange.startExpression, http.basePath] + ); + + return ( + <WrapperFlexGroup direction="column" gutterSize="m"> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="m"> + <EuiFlexItem> + <LogQueryBar + query={draftQuery} + onUpdateQuery={onUpdateDraftQuery} + isQueryValid={isDraftQueryValid} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFilterGroup> + <DatasetFilter + selectedDatasets={selectedDatasets} + onToggleDataset={(level: string) => { + const currentLevels = [...selectedDatasets]; + const levelPosition = currentLevels.indexOf(level); + if (levelPosition >= 0) { + currentLevels.splice(levelPosition, 1); + setSelectedDatasets(currentLevels); + } else { + setSelectedDatasets([...selectedDatasets, level]); + } + }} + /> + <LogLevelFilter + selectedLevels={selectedLogLevels} + onToggleLevel={(level: string) => { + const currentLevels = [...selectedLogLevels]; + const levelPosition = currentLevels.indexOf(level); + if (levelPosition >= 0) { + currentLevels.splice(levelPosition, 1); + setSelectedLogLevels(currentLevels); + } else { + setSelectedLogLevels([...selectedLogLevels, level]); + } + }} + /> + </EuiFilterGroup> + </EuiFlexItem> + <DatePickerFlexItem grow={false}> + <EuiSuperDatePicker + showUpdateButton={false} + start={dateRange.startExpression} + end={dateRange.endExpression} + onTimeChange={({ start, end }) => { + tryUpdateDateRange({ + from: start, + to: end, + }); + }} + /> + </DatePickerFlexItem> + <EuiFlexItem grow={false}> + <RedirectAppLinks application={application}> + <EuiButtonEmpty href={viewInLogsUrl} iconType="popout" flush="both"> + <FormattedMessage + id="xpack.fleet.agentLogs.openInLogsUiLinkText" + defaultMessage="Open in Logs" + /> + </EuiButtonEmpty> + </RedirectAppLinks> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiPanel paddingSize="none"> + <LogStream + height="100%" + startTimestamp={dateRange.startTimestamp} + endTimestamp={dateRange.endTimestamp} + query={logStreamQuery} + /> + </EuiPanel> + </EuiFlexItem> + </WrapperFlexGroup> + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx new file mode 100644 index 0000000000000..ae2385d714219 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + QueryStringInput, + IFieldType, +} from '../../../../../../../../../../../src/plugins/data/public'; +import { useStartServices } from '../../../../../hooks'; +import { + AGENT_LOG_INDEX_PATTERN, + AGENT_ID_FIELD, + DATASET_FIELD, + LOG_LEVEL_FIELD, +} from './constants'; + +const EXCLUDED_FIELDS = [AGENT_ID_FIELD.name, DATASET_FIELD.name, LOG_LEVEL_FIELD.name]; + +export const LogQueryBar: React.FunctionComponent<{ + query: string; + isQueryValid: boolean; + onUpdateQuery: (query: string, runQuery?: boolean) => void; +}> = memo(({ query, isQueryValid, onUpdateQuery }) => { + const { data } = useStartServices(); + const [indexPatternFields, setIndexPatternFields] = useState<IFieldType[]>(); + + useEffect(() => { + const fetchFields = async () => { + try { + const fields = ( + ((await data.indexPatterns.getFieldsForWildcard({ + pattern: AGENT_LOG_INDEX_PATTERN, + })) as IFieldType[]) || [] + ).filter((field) => { + return !EXCLUDED_FIELDS.includes(field.name); + }); + setIndexPatternFields(fields); + } catch (err) { + setIndexPatternFields(undefined); + } + }; + fetchFields(); + }, [data.indexPatterns]); + + return ( + <QueryStringInput + indexPatterns={ + indexPatternFields + ? [ + { + title: AGENT_LOG_INDEX_PATTERN, + fields: indexPatternFields, + }, + ] + : [] + } + query={{ + query, + language: 'kuery', + }} + isInvalid={!isQueryValid} + disableAutoFocus={true} + placeholder={i18n.translate('xpack.fleet.agentLogs.searchPlaceholderText', { + defaultMessage: 'Search logs…', + })} + onChange={(newQuery) => { + onUpdateQuery(newQuery.query as string); + }} + onSubmit={(newQuery) => { + onUpdateQuery(newQuery.query as string, true); + }} + /> + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/helper.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/helper.ts deleted file mode 100644 index b512ca230080d..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/helper.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AgentMetadata } from '../../../../types'; - -export function flattenMetadata(metadata: AgentMetadata) { - return Object.entries(metadata).reduce((acc, [key, value]) => { - if (typeof value === 'string') { - acc[key] = value; - - return acc; - } - - Object.entries(flattenMetadata(value)).forEach(([flattenedKey, flattenedValue]) => { - acc[`${key}.${flattenedKey}`] = flattenedValue; - }); - - return acc; - }, {} as { [k: string]: string }); -} -export function unflattenMetadata(flattened: { [k: string]: string }) { - const metadata: AgentMetadata = {}; - - Object.entries(flattened).forEach(([flattenedKey, flattenedValue]) => { - const keyParts = flattenedKey.split('.'); - const lastKey = keyParts.pop(); - - if (!lastKey) { - throw new Error('Invalid metadata'); - } - - let metadataPart = metadata; - keyParts.forEach((keyPart) => { - if (!metadataPart[keyPart]) { - metadataPart[keyPart] = {}; - } - - metadataPart = metadataPart[keyPart] as AgentMetadata; - }); - metadataPart[lastKey] = flattenedValue; - }); - - return metadata; -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/index.ts index 8e6ddd0959358..128f803bb2f2e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/index.ts @@ -3,6 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { AgentEventsTable } from './agent_events_table'; +export { AgentLogs } from './agent_logs'; export { AgentDetailsActionMenu } from './actions_menu'; export { AgentDetailsContent } from './agent_details'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/metadata_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/metadata_flyout.tsx deleted file mode 100644 index f808f4ade107b..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/metadata_flyout.tsx +++ /dev/null @@ -1,78 +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 { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiTitle, - EuiSpacer, - EuiDescriptionList, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiHorizontalRule, -} from '@elastic/eui'; -import { MetadataForm } from './metadata_form'; -import { Agent } from '../../../../types'; -import { flattenMetadata } from './helper'; - -interface Props { - agent: Agent; - flyout: { hide: () => void }; -} - -export const AgentMetadataFlyout: React.FunctionComponent<Props> = ({ agent, flyout }) => { - const mapMetadata = (obj: { [key: string]: string } | undefined) => { - return Object.keys(obj || {}).map((key) => ({ - title: key, - description: obj ? obj[key] : '', - })); - }; - - const localItems = mapMetadata(flattenMetadata(agent.local_metadata)); - const userProvidedItems = mapMetadata(flattenMetadata(agent.user_provided_metadata)); - - return ( - <EuiFlyout onClose={() => flyout.hide()} size="s" aria-labelledby="flyoutTitle"> - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2 id="flyoutTitle"> - <FormattedMessage - id="xpack.fleet.agentDetails.metadataSectionTitle" - defaultMessage="Metadata" - /> - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.fleet.agentDetails.localMetadataSectionSubtitle" - defaultMessage="Local metadata" - /> - </h3> - </EuiTitle> - <EuiHorizontalRule /> - <EuiDescriptionList type="column" compressed listItems={localItems} /> - <EuiSpacer size="xxl" /> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.fleet.agentDetails.userProvidedMetadataSectionSubtitle" - defaultMessage="User provided metadata" - /> - </h3> - </EuiTitle> - <EuiHorizontalRule /> - <EuiDescriptionList type="column" compressed listItems={userProvidedItems} /> - <EuiSpacer size="m" /> - - <MetadataForm agent={agent} /> - </EuiFlyoutBody> - </EuiFlyout> - ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/metadata_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/metadata_form.tsx deleted file mode 100644 index fd8de709c172a..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/metadata_form.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiPopover, - EuiFormRow, - EuiButton, - EuiFlexItem, - EuiFieldText, - EuiFlexGroup, - EuiForm, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { AxiosError } from 'axios'; -import { useAgentRefresh } from '../hooks'; -import { useInput, sendRequest } from '../../../../hooks'; -import { Agent } from '../../../../types'; -import { agentRouteService } from '../../../../services'; -import { flattenMetadata, unflattenMetadata } from './helper'; - -function useAddMetadataForm(agent: Agent, done: () => void) { - const refreshAgent = useAgentRefresh(); - const keyInput = useInput(); - const valueInput = useInput(); - const [state, setState] = useState<{ - isLoading: boolean; - error: null | string; - }>({ - isLoading: false, - error: null, - }); - - function clearInputs() { - keyInput.clear(); - valueInput.clear(); - } - - function setError(error: AxiosError) { - setState({ - isLoading: false, - error: error.response && error.response.data ? error.response.data.message : error.message, - }); - } - - async function success() { - await refreshAgent(); - setState({ - isLoading: false, - error: null, - }); - clearInputs(); - done(); - } - - return { - state, - onSubmit: async (e: React.FormEvent | React.MouseEvent) => { - e.preventDefault(); - setState({ - ...state, - isLoading: true, - }); - - const metadata = unflattenMetadata({ - ...flattenMetadata(agent.user_provided_metadata), - [keyInput.value]: valueInput.value, - }); - - try { - const { error } = await sendRequest({ - path: agentRouteService.getUpdatePath(agent.id), - method: 'put', - body: JSON.stringify({ - user_provided_metadata: metadata, - }), - }); - - if (error) { - throw error; - } - await success(); - } catch (error) { - setError(error); - } - }, - inputs: { - keyInput, - valueInput, - }, - }; -} - -export const MetadataForm: React.FunctionComponent<{ agent: Agent }> = ({ agent }) => { - const [isOpen, setOpen] = useState(false); - - const form = useAddMetadataForm(agent, () => { - setOpen(false); - }); - const { keyInput, valueInput } = form.inputs; - - const button = ( - <EuiButtonEmpty onClick={() => setOpen(true)} color={'text'}> - <FormattedMessage id="xpack.fleet.metadataForm.addButton" defaultMessage="+ Add metadata" /> - </EuiButtonEmpty> - ); - return ( - <> - <EuiPopover - id="trapFocus" - ownFocus - button={button} - isOpen={isOpen} - closePopover={() => setOpen(false)} - initialFocus="[id=fleet-details-metadata-form]" - > - <form onSubmit={form.onSubmit}> - <EuiForm error={form.state.error} isInvalid={form.state.error !== null}> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - id="fleet-details-metadata-form" - label={i18n.translate('xpack.fleet.metadataForm.keyLabel', { - defaultMessage: 'Key', - })} - > - <EuiFieldText required={true} {...keyInput.props} /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label={i18n.translate('xpack.fleet.metadataForm.valueLabel', { - defaultMessage: 'Value', - })} - > - <EuiFieldText required={true} {...valueInput.props} /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton isLoading={form.state.isLoading} type={'submit'}> - <FormattedMessage - id="xpack.fleet.metadataForm.submitButtonText" - defaultMessage="Add" - /> - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </form> - </EuiPopover> - </> - ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/type_labels.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/type_labels.tsx deleted file mode 100644 index dbe18ab333736..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/type_labels.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AgentEvent } from '../../../../types'; - -export const TYPE_LABEL: { [key in AgentEvent['type']]: JSX.Element } = { - STATE: ( - <EuiBadge color="hollow"> - <FormattedMessage id="xpack.fleet.agentEventType.stateLabel" defaultMessage="State" /> - </EuiBadge> - ), - ERROR: ( - <EuiBadge color="danger"> - <FormattedMessage id="xpack.fleet.agentEventType.errorLabel" defaultMessage="Error" /> - </EuiBadge> - ), - ACTION_RESULT: ( - <EuiBadge color="secondary"> - <FormattedMessage - id="xpack.fleet.agentEventType.actionResultLabel" - defaultMessage="Action result" - /> - </EuiBadge> - ), - ACTION: ( - <EuiBadge color="primary"> - <FormattedMessage id="xpack.fleet.agentEventType.actionLabel" defaultMessage="Action" /> - </EuiBadge> - ), -}; - -export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { - RUNNING: ( - <EuiBadge color="hollow"> - <FormattedMessage id="xpack.fleet.agentEventSubtype.runningLabel" defaultMessage="Running" /> - </EuiBadge> - ), - STARTING: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.startingLabel" - defaultMessage="Starting" - /> - </EuiBadge> - ), - IN_PROGRESS: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.inProgressLabel" - defaultMessage="In progress" - /> - </EuiBadge> - ), - CONFIG: ( - <EuiBadge color="hollow"> - <FormattedMessage id="xpack.fleet.agentEventSubtype.policyLabel" defaultMessage="Policy" /> - </EuiBadge> - ), - FAILED: ( - <EuiBadge color="hollow"> - <FormattedMessage id="xpack.fleet.agentEventSubtype.failedLabel" defaultMessage="Failed" /> - </EuiBadge> - ), - STOPPING: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.stoppingLabel" - defaultMessage="Stopping" - /> - </EuiBadge> - ), - STOPPED: ( - <EuiBadge color="hollow"> - <FormattedMessage id="xpack.fleet.agentEventSubtype.stoppedLabel" defaultMessage="Stopped" /> - </EuiBadge> - ), - DEGRADED: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.degradedLabel" - defaultMessage="Degraded" - /> - </EuiBadge> - ), - DATA_DUMP: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.dataDumpLabel" - defaultMessage="Data dump" - /> - </EuiBadge> - ), - ACKNOWLEDGED: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.acknowledgedLabel" - defaultMessage="Acknowledged" - /> - </EuiBadge> - ), - UPDATING: ( - <EuiBadge color="hollow"> - <FormattedMessage - id="xpack.fleet.agentEventSubtype.updatingLabel" - defaultMessage="Updating" - /> - </EuiBadge> - ), - UNKNOWN: ( - <EuiBadge color="hollow"> - <FormattedMessage id="xpack.fleet.agentEventSubtype.unknownLabel" defaultMessage="Unknown" /> - </EuiBadge> - ), -}; 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 7d60ae23deac6..f3714bbb53223 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 @@ -28,13 +28,13 @@ import { useGetOneAgentPolicy, useLink, useBreadcrumbs, - useCore, + useStartServices, useKibanaVersion, } from '../../../hooks'; import { WithHeaderLayout } from '../../../layouts'; import { AgentHealth } from '../components'; import { AgentRefreshContext } from './hooks'; -import { AgentEventsTable, AgentDetailsActionMenu, AgentDetailsContent } from './components'; +import { AgentLogs, AgentDetailsActionMenu, AgentDetailsContent } from './components'; import { useIntraAppState } from '../../../hooks/use_intra_app_state'; import { isAgentUpgradeable } from '../../../services'; @@ -67,7 +67,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { const { application: { navigateToApp }, - } = useCore(); + } = useStartServices(); const routeState = useIntraAppState<AgentDetailsReassignPolicyAction>(); const queryParams = new URLSearchParams(useLocation().search); const openReassignFlyoutOpenByDefault = queryParams.get('openReassignFlyout') === 'true'; @@ -223,21 +223,21 @@ export const AgentDetailsPage: React.FunctionComponent = () => { const headerTabs = useMemo(() => { return [ - { - id: 'activity_log', - name: i18n.translate('xpack.fleet.agentDetails.subTabs.activityLogTab', { - defaultMessage: 'Activity log', - }), - href: getHref('fleet_agent_details', { agentId, tabId: 'activity' }), - isSelected: !tabId || tabId === 'activity', - }, { id: 'details', name: i18n.translate('xpack.fleet.agentDetails.subTabs.detailsTab', { defaultMessage: 'Agent details', }), href: getHref('fleet_agent_details', { agentId, tabId: 'details' }), - isSelected: tabId === 'details', + isSelected: !tabId || tabId === 'details', + }, + { + id: 'logs', + name: i18n.translate('xpack.fleet.agentDetails.subTabs.logsTab', { + defaultMessage: 'Logs', + }), + href: getHref('fleet_agent_details', { agentId, tabId: 'logs' }), + isSelected: tabId === 'logs', }, ]; }, [getHref, agentId, tabId]); @@ -305,15 +305,15 @@ const AgentDetailsPageContent: React.FunctionComponent<{ return ( <Switch> <Route - path={PAGE_ROUTING_PATHS.fleet_agent_details_details} + path={PAGE_ROUTING_PATHS.fleet_agent_details_logs} render={() => { - return <AgentDetailsContent agent={agent} agentPolicy={agentPolicy} />; + return <AgentLogs agent={agent} />; }} /> <Route - path={PAGE_ROUTING_PATHS.fleet_agent_details_events} + path={PAGE_ROUTING_PATHS.fleet_agent_details} render={() => { - return <AgentEventsTable agent={agent} />; + return <AgentDetailsContent agent={agent} agentPolicy={agentPolicy} />; }} /> </Switch> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx index 758497607c057..b90758335dc75 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import { SO_SEARCH_LIMIT } from '../../../../constants'; import { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../../../types'; -import { sendGetEnrollmentAPIKeys, useCore } from '../../../../hooks'; +import { sendGetEnrollmentAPIKeys, useStartServices } from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; type Props = { @@ -27,7 +27,7 @@ type Props = { ); export const EnrollmentStepAgentPolicy: React.FC<Props> = (props) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const { withKeySelection, agentPolicies, onAgentPolicyChange } = props; const onKeyChange = props.withKeySelection && props.onKeyChange; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx index 656493e31e5f5..840e47c5cd1f7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../../types'; import { useGetOneEnrollmentAPIKey, - useCore, + useStartServices, useGetSettings, useLink, useFleetStatus, @@ -26,7 +26,7 @@ interface Props { export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => { const { getHref } = useLink(); - const core = useCore(); + const core = useStartServices(); const fleetStatus = useFleetStatus(); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | undefined>(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx index a2daf2d10c271..da2bb8adf1b35 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -21,7 +21,7 @@ import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../../types'; -import { useCore, useLink, sendGetOneAgentPolicyFull } from '../../../../hooks'; +import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../../../hooks'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../../services'; @@ -33,7 +33,7 @@ const RUN_INSTRUCTIONS = './elastic-agent install'; export const StandaloneInstructions = React.memo<Props>(({ agentPolicies }) => { const { getHref } = useLink(); - const core = useCore(); + const core = useStartServices(); const { notifications } = core; const [selectedPolicyId, setSelectedPolicyId] = useState<string | undefined>(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx index 46e291e73fa78..90726b54d283a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx @@ -25,7 +25,7 @@ import { Agent } from '../../../../types'; import { sendPutAgentReassign, sendPostBulkAgentReassign, - useCore, + useStartServices, useGetAgentPolicies, } from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; @@ -39,7 +39,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent<Props> = ({ onClose, agents, }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const isSingleAgent = Array.isArray(agents) && agents.length === 1; const [selectedAgentPolicyId, setSelectedAgentPolicyId] = useState<string | undefined>( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx index 1b3935a86f65c..180ad5e4953b8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx @@ -8,7 +8,11 @@ import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, EuiOverlayMask, EuiFormFieldset, EuiCheckbox } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; -import { sendPostAgentUnenroll, sendPostBulkAgentUnenroll, useCore } from '../../../../hooks'; +import { + sendPostAgentUnenroll, + sendPostBulkAgentUnenroll, + useStartServices, +} from '../../../../hooks'; interface Props { onClose: () => void; @@ -23,7 +27,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({ agentCount, useForceUnenroll, }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [forceUnenroll, setForceUnenroll] = useState<boolean>(useForceUnenroll || false); const [isSubmitting, setIsSubmitting] = useState(false); const isSingleAgent = Array.isArray(agents) && agents.length === 1; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 43ad7208c3d81..6b7fca9e086aa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -14,7 +14,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; -import { sendPostAgentUpgrade, sendPostBulkAgentUpgrade, useCore } from '../../../../hooks'; +import { + sendPostAgentUpgrade, + sendPostBulkAgentUpgrade, + useStartServices, +} from '../../../../hooks'; interface Props { onClose: () => void; @@ -29,7 +33,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({ agentCount, version, }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [isSubmitting, setIsSubmitting] = useState(false); const isSingleAgent = Array.isArray(agents) && agents.length === 1; async function onSubmit() { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx index 78e8be4679dc3..ed607e361bd6e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx @@ -22,14 +22,14 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../../types'; -import { useInput, useCore, sendRequest } from '../../../../hooks'; +import { useInput, useStartServices, sendRequest } from '../../../../hooks'; import { enrollmentAPIKeyRouteService } from '../../../../services'; function useCreateApiKeyForm( policyIdDefaultValue: string | undefined, onSuccess: (keyId: string) => void ) { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [isLoading, setIsLoading] = useState(false); const apiKeyNameInput = useInput(''); const policyIdInput = useInput(policyIdDefaultValue); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx index 7e5d07b2319d3..71cd417a256c3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx @@ -26,7 +26,7 @@ import { useGetEnrollmentAPIKeys, useGetAgentPolicies, sendGetOneEnrollmentAPIKey, - useCore, + useStartServices, sendDeleteOneEnrollmentAPIKey, } from '../../../hooks'; import { EnrollmentAPIKey } from '../../../types'; @@ -35,7 +35,7 @@ import { NewEnrollmentTokenFlyout } from './components/new_enrollment_key_flyout import { ConfirmEnrollmentTokenDelete } from './components/confirm_delete_modal'; const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [state, setState] = useState<'VISIBLE' | 'HIDDEN' | 'LOADING'>('HIDDEN'); const [key, setKey] = useState<string | undefined>(); @@ -106,7 +106,7 @@ const DeleteButton: React.FunctionComponent<{ apiKey: EnrollmentAPIKey; refresh: apiKey, refresh, }) => { - const { notifications } = useCore(); + const { notifications } = useStartServices(); const [state, setState] = useState<'CONFIRM_VISIBLE' | 'CONFIRM_HIDDEN'>('CONFIRM_HIDDEN'); const onCancel = () => setState('CONFIRM_HIDDEN'); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/setup_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/setup_page/index.tsx index 60ee791ace5eb..8fee44018f0a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/setup_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/setup_page/index.tsx @@ -22,7 +22,7 @@ import { EuiCodeBlock, EuiLink, } from '@elastic/eui'; -import { useCore, sendPostFleetSetup } from '../../../hooks'; +import { useStartServices, sendPostFleetSetup } from '../../../hooks'; import { WithoutHeaderLayout } from '../../../layouts'; import { GetFleetStatusResponse } from '../../../types'; @@ -53,7 +53,7 @@ export const SetupPage: React.FunctionComponent<{ missingRequirements: GetFleetStatusResponse['missing_requirements']; }> = ({ refresh, missingRequirements }) => { const [isFormLoading, setIsFormLoading] = useState<boolean>(false); - const core = useCore(); + const core = useStartServices(); const onSubmit = async () => { setIsFormLoading(true); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx index 533c273681122..c614518c1930b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { DataStream } from '../../../types'; import { WithHeaderLayout } from '../../../layouts'; -import { useGetDataStreams, useStartDeps, usePagination, useBreadcrumbs } from '../../../hooks'; +import { useGetDataStreams, useStartServices, usePagination, useBreadcrumbs } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; import { DataStreamRowActions } from './components/data_stream_row_actions'; @@ -59,7 +59,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { const { data: { fieldFormats }, - } = useStartDeps(); + } = useStartServices(); const { pagination, pageSizeOptions } = usePagination(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/icon_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/icon_panel.tsx index 7004a602627c1..8ced0734a3967 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/icon_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/icon_panel.tsx @@ -12,8 +12,9 @@ import { Loading } from '../../../components'; const PanelWrapper = styled.div` // NOTE: changes to the width here will impact navigation tabs page layout under integration package details width: ${(props) => - parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.spacerSizes.xl) * 2}px; + parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; height: 1px; + z-index: 1; `; const Panel = styled(EuiPanel)` diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx index a453a7f2e28cb..3d2babae8eb2e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useCore } from '../../../hooks/use_core'; +import { useStartServices } from '../../../hooks/use_core'; import { PLUGIN_ID } from '../../../constants'; import { epmRouteService } from '../../../services'; @@ -11,7 +11,7 @@ const removeRelativePath = (relativePath: string): string => new URL(relativePath, 'http://example.com').pathname; export function useLinks() { - const { http } = useCore(); + const { http } = useStartServices(); return { toAssets: (path: string) => http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), toImage: (path: string) => http.basePath.prepend(epmRouteService.getFilePath(path)), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.scss b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.scss new file mode 100644 index 0000000000000..e8366d99b6391 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.scss @@ -0,0 +1,5 @@ +@import '@elastic/eui/src/global_styling/variables/_size.scss'; + +.fleet__epm__shiftNavTabs { + margin-left: $euiSize * 6 + $euiSizeXL * 2 + $euiSizeL; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index 2535a53589bd9..0e72693db9e2d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -28,13 +28,13 @@ import { useLink, useCapabilities, } from '../../../../hooks'; -import { WithHeaderLayout } from '../../../../layouts'; +import { WithHeaderLayout, WithHeaderLayoutProps } from '../../../../layouts'; import { useSetPackageInstallStatus } from '../../hooks'; import { IconPanel, LoadingIconPanel } from '../../components/icon_panel'; import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge'; import { UpdateIcon } from '../../components/icons'; import { Content } from './content'; -import { WithHeaderLayoutProps } from '../../../../layouts/with_header'; +import './index.scss'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -55,16 +55,6 @@ const PanelDisplayNames: Record<DetailViewPanelName, string> = { }), }; -const DetailWrapper = styled.div` - // Class name here is in sync with 'PanelWrapper' in 'IconPanel' component - .shiftNavTabs { - margin-left: ${(props) => - parseFloat(props.theme.eui.euiSize) * 6 + - parseFloat(props.theme.eui.spacerSizes.xl) * 2 + - parseFloat(props.theme.eui.spacerSizes.l)}px; - } -`; - const Divider = styled.div` width: 0; height: 100%; @@ -265,31 +255,29 @@ export function Detail() { }, [getHref, packageInfo, packageInfoData?.response?.status, panel]); return ( - <DetailWrapper> - <WithHeaderLayout - leftColumn={headerLeftContent} - rightColumn={headerRightContent} - rightColumnGrow={false} - tabs={tabs} - tabsClassName={'shiftNavTabs'} - > - {packageInfo ? <Breadcrumbs packageTitle={packageInfo.title} /> : null} - {packageInfoError ? ( - <Error - title={ - <FormattedMessage - id="xpack.fleet.epm.loadingIntegrationErrorTitle" - defaultMessage="Error loading integration details" - /> - } - error={packageInfoError} - /> - ) : isLoading || !packageInfo ? ( - <Loading /> - ) : ( - <Content {...packageInfo} panel={panel} /> - )} - </WithHeaderLayout> - </DetailWrapper> + <WithHeaderLayout + leftColumn={headerLeftContent} + rightColumn={headerRightContent} + rightColumnGrow={false} + tabs={tabs} + tabsClassName="fleet__epm__shiftNavTabs" + > + {packageInfo ? <Breadcrumbs packageTitle={packageInfo.title} /> : null} + {packageInfoError ? ( + <Error + title={ + <FormattedMessage + id="xpack.fleet.epm.loadingIntegrationErrorTitle" + defaultMessage="Error loading integration details" + /> + } + error={packageInfoError} + /> + ) : isLoading || !packageInfo ? ( + <Loading /> + ) : ( + <Content {...packageInfo} panel={panel} /> + )} + </WithHeaderLayout> ); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/header.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/header.tsx index e9704cd16b219..b5fef901d123d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/header.tsx @@ -10,7 +10,7 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useLinks } from '../../hooks'; -import { useCore } from '../../../../hooks'; +import { useStartServices } from '../../../../hooks'; export const HeroCopy = memo(() => { return ( @@ -43,7 +43,7 @@ const Illustration = styled(EuiImage)` export const HeroImage = memo(() => { const { toAssets } = useLinks(); - const { uiSettings } = useCore(); + const { uiSettings } = useStartServices(); const IS_DARK_THEME = uiSettings.get('theme:darkMode'); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/datastream_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/datastream_section.tsx index 58f84e8671385..10f538b3112c6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/datastream_section.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/datastream_section.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; -import { useLink, useGetDataStreams, useStartDeps } from '../../../hooks'; +import { useLink, useGetDataStreams, useStartServices } from '../../../hooks'; import { Loading } from '../../agents/components'; export const OverviewDatastreamSection: React.FC = () => { @@ -23,7 +23,7 @@ export const OverviewDatastreamSection: React.FC = () => { const datastreamRequest = useGetDataStreams(); const { data: { fieldFormats }, - } = useStartDeps(); + } = useStartServices(); const total = datastreamRequest.data?.data_streams?.length ?? 0; let sizeBytes = 0; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 7e523b3fa594a..31b53f41b3a91 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -17,6 +17,7 @@ import { HomePublicPluginSetup, FeatureCatalogueCategory, } from '../../../../src/plugins/home/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { PLUGIN_ID, CheckPermissionsResponse, PostIngestSetupResponse } from '../common'; import { BASE_PATH } from './applications/fleet/constants'; @@ -58,10 +59,15 @@ export interface FleetStartDeps { data: DataPublicPluginStart; } +export interface FleetStartServices extends CoreStart, FleetStartDeps { + storage: Storage; +} + export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDeps, FleetStartDeps> { private config: FleetConfigType; private kibanaVersion: string; private extensions: UIExtensionsStorage = {}; + private storage = new Storage(localStorage); constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get<FleetConfigType>(); @@ -86,26 +92,23 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep title: i18n.translate('xpack.fleet.appTitle', { defaultMessage: 'Fleet' }), order: 9020, euiIconType: 'logoElastic', - async mount(params: AppMountParameters) { - const [coreStart, startDeps] = (await core.getStartServices()) as [ + mount: async (params: AppMountParameters) => { + const [coreStartServices, startDepsServices] = (await core.getStartServices()) as [ CoreStart, FleetStartDeps, FleetStart ]; - const { renderApp, teardownFleet } = await import('./applications/fleet/'); - const unmount = renderApp( - coreStart, - params, - deps, - startDeps, - config, - kibanaVersion, - extensions - ); + const startServices: FleetStartServices = { + ...coreStartServices, + ...startDepsServices, + storage: this.storage, + }; + const { renderApp, teardownFleet } = await import('./applications/fleet'); + const unmount = renderApp(startServices, params, config, kibanaVersion, extensions); return () => { unmount(); - teardownFleet(coreStart); + teardownFleet(startServices); }; }, }); diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 3d30acd3f8e01..1fe7013944fd7 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -13,7 +13,13 @@ import { } from '../common'; export { default as apm } from 'elastic-apm-node'; -export { AgentService, ESIndexPatternService, getRegistryUrl, PackageService } from './services'; +export { + AgentService, + ESIndexPatternService, + getRegistryUrl, + PackageService, + AgentPolicyServiceInterface, +} from './services'; export { FleetSetupContract, FleetSetupDeps, FleetStartContract, ExternalCallback } from './plugin'; export const config: PluginConfigDescriptor = { diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index c8aef287e4432..91098c87c312a 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -9,6 +9,7 @@ import { FleetAppContext } from './plugin'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../security/server/mocks'; import { PackagePolicyServiceInterface } from './services/package_policy'; +import { AgentPolicyServiceInterface, AgentService } from './services'; export const createAppContextStartContractMock = (): FleetAppContext => { return { @@ -35,3 +36,28 @@ export const createPackagePolicyServiceMock = () => { update: jest.fn(), } as jest.Mocked<PackagePolicyServiceInterface>; }; + +/** + * Create mock AgentPolicyService + */ + +export const createMockAgentPolicyService = (): jest.Mocked<AgentPolicyServiceInterface> => { + return { + get: jest.fn(), + list: jest.fn(), + getDefaultAgentPolicyId: jest.fn(), + getFullAgentPolicy: jest.fn(), + }; +}; + +/** + * Creates a mock AgentService + */ +export const createMockAgentService = (): jest.Mocked<AgentService> => { + return { + getAgentStatusById: jest.fn(), + authenticateAgentWithAccessToken: jest.fn(), + getAgent: jest.fn(), + listAgents: jest.fn(), + }; +}; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index e4ed386802c3a..90fb34efd4817 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -58,6 +58,8 @@ import { ESIndexPatternSavedObjectService, ESIndexPatternService, AgentService, + AgentPolicyServiceInterface, + agentPolicyService, packagePolicyService, PackageService, } from './services'; @@ -134,6 +136,7 @@ export interface FleetStartContract { * Services for Fleet's package policies */ packagePolicyService: typeof packagePolicyService; + agentPolicyService: AgentPolicyServiceInterface; /** * Register callbacks for inclusion in fleet API processing * @param args @@ -292,6 +295,12 @@ export class FleetPlugin getAgentStatusById, authenticateAgentWithAccessToken, }, + agentPolicyService: { + get: agentPolicyService.get, + list: agentPolicyService.list, + getDefaultAgentPolicyId: agentPolicyService.getDefaultAgentPolicyId, + getFullAgentPolicy: agentPolicyService.getFullAgentPolicy, + }, packagePolicyService, registerExternalCallback: (...args: ExternalCallback) => { return appContextService.addExternalCallback(...args); diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts index 4574bcc64d4ce..2f08846642985 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts @@ -25,7 +25,6 @@ describe('test actions handlers schema', () => { NewAgentActionSchema.validate({ type: 'POLICY_CHANGE', data: 'data', - sent_at: '2020-03-14T19:45:02.620Z', }) ).toBeTruthy(); }); @@ -34,7 +33,6 @@ describe('test actions handlers schema', () => { expect(() => { NewAgentActionSchema.validate({ data: 'data', - sent_at: '2020-03-14T19:45:02.620Z', }); }).toThrowError(); }); @@ -55,7 +53,6 @@ describe('test actions handlers', () => { action: { type: 'POLICY_CHANGE', data: 'data', - sent_at: '2020-03-14T19:45:02.620Z', }, }, params: { diff --git a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts index 280c34744289e..04aa1767b4f14 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts @@ -20,8 +20,7 @@ export interface SharedKey { } type SharedKeyString = string; -type ArchiveFilelist = string[]; -const archiveFilelistCache: Map<SharedKeyString, ArchiveFilelist> = new Map(); +const archiveFilelistCache: Map<SharedKeyString, string[]> = new Map(); export const getArchiveFilelist = (keyArgs: SharedKey) => archiveFilelistCache.get(sharedKey(keyArgs)); @@ -46,6 +45,15 @@ export const getPackageInfo = (args: SharedKey) => { } }; +export const getArchivePackage = (args: SharedKey) => { + const packageInfo = getPackageInfo(args); + const paths = getArchiveFilelist(args); + return { + paths, + packageInfo, + }; +}; + export const setPackageInfo = ({ name, version, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 2d4a94a2332d6..3df2d39419ab8 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -7,10 +7,11 @@ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; import { isPackageLimited, installationStatuses } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { ValueOf } from '../../../../common/types'; +import { ArchivePackage, InstallSource, RegistryPackage, ValueOf } from '../../../../common/types'; import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; +import { getArchivePackage } from '../archive'; export { fetchFile as getFile, SearchParams } from '../registry'; @@ -109,23 +110,53 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise<PackageInfo> { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [savedObject, latestPackage, { paths: assets, packageInfo: item }] = await Promise.all([ + const [savedObject, latestPackage] = await Promise.all([ getInstallationObject({ savedObjectsClient, pkgName }), Registry.fetchFindLatestPackage(pkgName), - Registry.getRegistryPackage(pkgName, pkgVersion), ]); - // add properties that aren't (or aren't yet) on Registry response + const getPackageRes = await getPackageFromSource({ + pkgName, + pkgVersion, + pkgInstallSource: savedObject?.attributes.install_source, + }); + const paths = getPackageRes.paths; + const packageInfo = getPackageRes.packageInfo; + + // add properties that aren't (or aren't yet) on the package const updated = { - ...item, + ...packageInfo, latestVersion: latestPackage.version, - title: item.title || nameAsTitle(item.name), - assets: Registry.groupPathsByService(assets || []), + title: packageInfo.title || nameAsTitle(packageInfo.name), + assets: Registry.groupPathsByService(paths || []), removable: !isRequiredPackage(pkgName), }; return createInstallableFrom(updated, savedObject); } +// gets package from install_source if it exists otherwise gets from registry +export async function getPackageFromSource(options: { + pkgName: string; + pkgVersion: string; + pkgInstallSource?: InstallSource; +}): Promise<{ paths: string[] | undefined; packageInfo: RegistryPackage | ArchivePackage }> { + const { pkgName, pkgVersion, pkgInstallSource } = options; + // TODO: Check package storage before checking registry + let res; + if (pkgInstallSource === 'upload') { + res = getArchivePackage({ + name: pkgName, + version: pkgVersion, + installSource: pkgInstallSource, + }); + if (!res.packageInfo) + throw new Error(`installed package ${pkgName}-${pkgVersion} does not exist in cache`); + } else { + res = await Registry.getRegistryPackage(pkgName, pkgVersion); + } + return res; +} + export async function getInstallationObject(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 7a62c307973c2..d9015c5195536 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -9,6 +9,7 @@ import { AgentStatus, Agent, EsAssetReference } from '../types'; import * as settingsService from './settings'; import { getAgent, listAgents } from './agents'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; +import { agentPolicyService } from './agent_policy'; export { getRegistryUrl } from './epm/registry/registry_url'; @@ -59,6 +60,13 @@ export interface AgentService { listAgents: typeof listAgents; } +export interface AgentPolicyServiceInterface { + get: typeof agentPolicyService['get']; + list: typeof agentPolicyService['list']; + getDefaultAgentPolicyId: typeof agentPolicyService['getDefaultAgentPolicyId']; + getFullAgentPolicy: typeof agentPolicyService['getFullAgentPolicy']; +} + // Saved object services export { agentPolicyService } from './agent_policy'; export { packagePolicyService } from './package_policy'; diff --git a/x-pack/plugins/fleet/server/types/models/agent.ts b/x-pack/plugins/fleet/server/types/models/agent.ts index 98ed793604954..619c21d8bf5d9 100644 --- a/x-pack/plugins/fleet/server/types/models/agent.ts +++ b/x-pack/plugins/fleet/server/types/models/agent.ts @@ -62,14 +62,26 @@ export const AgentEventSchema = schema.object({ id: schema.string(), }); -export const NewAgentActionSchema = schema.object({ - type: schema.oneOf([ - schema.literal('POLICY_CHANGE'), - schema.literal('UNENROLL'), - schema.literal('UPGRADE'), - schema.literal('INTERNAL_POLICY_REASSIGN'), - ]), - data: schema.maybe(schema.any()), - ack_data: schema.maybe(schema.any()), - sent_at: schema.maybe(schema.string()), -}); +export const NewAgentActionSchema = schema.oneOf([ + schema.object({ + type: schema.oneOf([ + schema.literal('POLICY_CHANGE'), + schema.literal('UNENROLL'), + schema.literal('UPGRADE'), + schema.literal('INTERNAL_POLICY_REASSIGN'), + ]), + data: schema.maybe(schema.any()), + ack_data: schema.maybe(schema.any()), + }), + schema.object({ + type: schema.oneOf([schema.literal('SETTINGS')]), + data: schema.object({ + log_level: schema.oneOf([ + schema.literal('debug'), + schema.literal('info'), + schema.literal('warning'), + schema.literal('error'), + ]), + }), + }), +]); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 00c7d705c1f44..68b2ac59d2a19 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -195,3 +195,41 @@ export const POLICY_WITH_NODE_ROLE_ALLOCATION: PolicyFromES = { }, name: POLICY_NAME, }; + +export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ + version: 1, + modified_date: Date.now().toString(), + policy: { + foo: 'bar', + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + unknown_setting: 123, + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + my_unfollow_action: {}, + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + }, + delete: { + wait_for_snapshot: { + policy: SNAPSHOT_POLICY_NAME, + }, + delete: { + delete_searchable_snapshot: true, + }, + }, + }, + name: POLICY_NAME, + }, + name: POLICY_NAME, +} as any) as PolicyFromES; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index c91ee3e2a1c06..a203a434bb21a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -19,6 +19,7 @@ import { POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, POLICY_WITH_NODE_ROLE_ALLOCATION, + POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, getDefaultHotPhasePolicy, } from './constants'; @@ -31,6 +32,70 @@ describe('<EditPolicy />', () => { server.restore(); }); + describe('serialization', () => { + /** + * We assume that policies that populate this form are loaded directly from ES and so + * are valid according to ES. There may be settings in the policy created through the ILM + * API that the UI does not cater for, like the unfollow action. We do not want to overwrite + * the configuration for these actions in the UI. + */ + it('preserves policy settings it did not configure', async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + + // Set max docs to test whether we keep the unknown fields in that object after serializing + await actions.hot.setMaxDocs('1000'); + // Remove the delete phase to ensure that we also correctly remove data + await actions.delete.enable(false); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + foo: 'bar', // Made up value + name: 'my_policy', + phases: { + hot: { + actions: { + rollover: { + max_docs: 1000, + max_size: '50gb', + unknown_setting: 123, // Made up setting that should stay preserved + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + warm: { + actions: { + my_unfollow_action: {}, // Made up action + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + min_age: '0ms', + }, + }, + }); + }); + }); + describe('hot phase', () => { describe('serialization', () => { beforeEach(async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 3e1577d8033ba..eb17402a46950 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -298,12 +298,12 @@ describe('edit policy', () => { phases: { hot: { actions: { - set_priority: { - priority: 100, - }, rollover: { - max_size: '50gb', max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, }, }, min_age: '0ms', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 5af8807f2dec8..df5d6e2f80c15 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -22,13 +22,11 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { const _meta: FormInternal['_meta'] = { hot: { useRollover: Boolean(hot?.actions?.rollover), - forceMergeEnabled: Boolean(hot?.actions?.forcemerge), bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', }, warm: { enabled: Boolean(warm), warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), - forceMergeEnabled: Boolean(warm?.actions?.forcemerge), bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', dataTierAllocationType: determineDataTierAllocationType(warm?.actions), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts new file mode 100644 index 0000000000000..b379cb3956a02 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setAutoFreeze } from 'immer'; +import { cloneDeep } from 'lodash'; +import { SerializedPolicy } from '../../../../../common/types'; +import { deserializer } from './deserializer'; +import { createSerializer } from './serializer'; +import { FormInternal } from '../types'; + +const isObject = (v: unknown): v is { [key: string]: any } => + Object.prototype.toString.call(v) === '[object Object]'; + +const unknownValue = { some: 'value' }; + +const populateWithUnknownEntries = (v: unknown) => { + if (isObject(v)) { + for (const key of Object.keys(v)) { + if (['require', 'include', 'exclude'].includes(key)) continue; // this will generate an invalid policy + populateWithUnknownEntries(v[key]); + } + v.unknown = unknownValue; + return; + } + if (Array.isArray(v)) { + v.forEach(populateWithUnknownEntries); + } +}; + +const originalPolicy: SerializedPolicy = { + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '1d', + max_size: '10gb', + max_docs: 1000, + }, + forcemerge: { + index_codec: 'best_compression', + max_num_segments: 22, + }, + set_priority: { + priority: 1, + }, + }, + min_age: '12ms', + }, + warm: { + min_age: '12ms', + actions: { + shrink: { number_of_shards: 12 }, + allocate: { + number_of_replicas: 3, + }, + set_priority: { + priority: 10, + }, + migrate: { enabled: false }, + }, + }, + cold: { + min_age: '30ms', + actions: { + allocate: { + number_of_replicas: 12, + require: { test: 'my_value' }, + include: { test: 'my_value' }, + exclude: { test: 'my_value' }, + }, + freeze: {}, + set_priority: { + priority: 12, + }, + }, + }, + delete: { + min_age: '33ms', + actions: { + delete: { + delete_searchable_snapshot: true, + }, + wait_for_snapshot: { + policy: 'test', + }, + }, + }, + }, +}; + +describe('deserializer and serializer', () => { + let policy: SerializedPolicy; + let serializer: ReturnType<typeof createSerializer>; + let formInternal: FormInternal; + + // So that we can modify produced form objects + beforeAll(() => setAutoFreeze(false)); + // This is the default in dev, so change back to true (https://github.com/immerjs/immer/blob/master/docs/freezing.md) + afterAll(() => setAutoFreeze(true)); + + beforeEach(() => { + policy = cloneDeep(originalPolicy); + formInternal = deserializer(policy); + // Because the policy object is not deepCloned by the form lib we + // clone here so that we can mutate the policy and preserve the + // original reference in the createSerializer + serializer = createSerializer(cloneDeep(policy)); + }); + + it('preserves any unknown policy settings', () => { + const thisTestPolicy = cloneDeep(originalPolicy); + // We populate all levels of the policy with entries our UI does not know about + populateWithUnknownEntries(thisTestPolicy); + serializer = createSerializer(thisTestPolicy); + + const copyOfThisTestPolicy = cloneDeep(thisTestPolicy); + + expect(serializer(deserializer(thisTestPolicy))).toEqual(thisTestPolicy); + + // Assert that the policy we passed in is unaltered after deserialization and serialization + expect(thisTestPolicy).not.toBe(copyOfThisTestPolicy); + expect(thisTestPolicy).toEqual(copyOfThisTestPolicy); + }); + + it('removes all phases if they were disabled in the form', () => { + formInternal._meta.warm.enabled = false; + formInternal._meta.cold.enabled = false; + formInternal._meta.delete.enabled = false; + + expect(serializer(formInternal)).toEqual({ + name: 'test', + phases: { + hot: policy.phases.hot, // We expect to see only the hot phase + }, + }); + }); + + it('removes the forcemerge action if it is disabled in the form', () => { + delete formInternal.phases.hot!.actions.forcemerge; + delete formInternal.phases.warm!.actions.forcemerge; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + expect(result.phases.warm!.actions.forcemerge).toBeUndefined(); + }); + + it('removes set priority if it is disabled in the form', () => { + delete formInternal.phases.hot!.actions.set_priority; + delete formInternal.phases.warm!.actions.set_priority; + delete formInternal.phases.cold!.actions.set_priority; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.set_priority).toBeUndefined(); + expect(result.phases.warm!.actions.set_priority).toBeUndefined(); + expect(result.phases.cold!.actions.set_priority).toBeUndefined(); + }); + + it('removes freeze setting in the cold phase if it is disabled in the form', () => { + formInternal._meta.cold.freezeEnabled = false; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.freeze).toBeUndefined(); + }); + + it('removes node attribute allocation when it is not selected in the form', () => { + // Change from 'node_attrs' to 'node_roles' + formInternal._meta.cold.dataTierAllocationType = 'node_roles'; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.allocate!.number_of_replicas).toBe(12); + expect(result.phases.cold!.actions.allocate!.require).toBeUndefined(); + expect(result.phases.cold!.actions.allocate!.include).toBeUndefined(); + expect(result.phases.cold!.actions.allocate!.exclude).toBeUndefined(); + }); + + it('removes forcemerge and rollover config when rollover is disabled in hot phase', () => { + formInternal._meta.hot.useRollover = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.rollover).toBeUndefined(); + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + }); + + it('removes min_age from warm when rollover is enabled', () => { + formInternal._meta.hot.useRollover = true; + formInternal._meta.warm.warmPhaseOnRollover = true; + + const result = serializer(formInternal); + + expect(result.phases.warm!.min_age).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 4d20db4018740..0ad2d923117f4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -23,7 +23,7 @@ import { i18nTexts } from '../i18n_texts'; const { emptyField, numberGreaterThanField } = fieldValidators; const serializers = { - stringToNumber: (v: string): any => (v ? parseInt(v, 10) : undefined), + stringToNumber: (v: string): any => (v != null ? parseInt(v, 10) : undefined), }; export const schema: FormSchema<FormInternal> = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts deleted file mode 100644 index 2274efda426ad..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts +++ /dev/null @@ -1,185 +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 { isEmpty, isNumber } from 'lodash'; - -import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../../common/types'; - -import { FormInternal, DataAllocationMetaFields } from '../types'; - -const serializeAllocateAction = ( - { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, - newActions: SerializedActionWithAllocation = {}, - originalActions: SerializedActionWithAllocation = {} -): SerializedActionWithAllocation => { - const { allocate, migrate, ...rest } = newActions; - // First copy over all non-allocate and migrate actions. - const actions: SerializedActionWithAllocation = { allocate, migrate, ...rest }; - - switch (dataTierAllocationType) { - case 'node_attrs': - if (allocationNodeAttribute) { - const [name, value] = allocationNodeAttribute.split(':'); - actions.allocate = { - // copy over any other allocate details like "number_of_replicas" - ...actions.allocate, - require: { - [name]: value, - }, - }; - } else { - // The form has been configured to use node attribute based allocation but no node attribute - // was selected. We fall back to what was originally selected in this case. This might be - // migrate.enabled: "false" - actions.migrate = originalActions.migrate; - } - - // copy over the original include and exclude values until we can set them in the form. - if (!isEmpty(originalActions?.allocate?.include)) { - actions.allocate = { - ...actions.allocate, - include: { ...originalActions?.allocate?.include }, - }; - } - - if (!isEmpty(originalActions?.allocate?.exclude)) { - actions.allocate = { - ...actions.allocate, - exclude: { ...originalActions?.allocate?.exclude }, - }; - } - break; - case 'none': - actions.migrate = { enabled: false }; - break; - default: - } - return actions; -}; - -export const createSerializer = (originalPolicy?: SerializedPolicy) => ( - data: FormInternal -): SerializedPolicy => { - const { _meta, ...policy } = data; - - if (!policy.phases || !policy.phases.hot) { - policy.phases = { hot: { actions: {} } }; - } - - /** - * HOT PHASE SERIALIZATION - */ - if (policy.phases.hot) { - policy.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms'; - } - - if (policy.phases.hot?.actions) { - if (policy.phases.hot.actions?.rollover && _meta.hot.useRollover) { - if (policy.phases.hot.actions.rollover.max_age) { - policy.phases.hot.actions.rollover.max_age = `${policy.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`; - } - - if (policy.phases.hot.actions.rollover.max_size) { - policy.phases.hot.actions.rollover.max_size = `${policy.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; - } - - if (_meta.hot.bestCompression && policy.phases.hot.actions?.forcemerge) { - policy.phases.hot.actions.forcemerge.index_codec = 'best_compression'; - } - } else { - delete policy.phases.hot.actions?.rollover; - } - } - - /** - * WARM PHASE SERIALIZATION - */ - if (policy.phases.warm) { - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive - if (_meta.hot.useRollover && _meta.warm.warmPhaseOnRollover) { - delete policy.phases.warm.min_age; - } else if ( - (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && - policy.phases.warm.min_age - ) { - policy.phases.warm.min_age = `${policy.phases.warm.min_age}${_meta.warm.minAgeUnit}`; - } - - policy.phases.warm.actions = serializeAllocateAction( - _meta.warm, - policy.phases.warm.actions, - originalPolicy?.phases.warm?.actions - ); - - if ( - policy.phases.warm.actions.allocate && - !policy.phases.warm.actions.allocate.require && - !isNumber(policy.phases.warm.actions.allocate.number_of_replicas) && - isEmpty(policy.phases.warm.actions.allocate.include) && - isEmpty(policy.phases.warm.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete policy.phases.warm.actions.allocate; - } - - if (_meta.warm.bestCompression && policy.phases.warm.actions?.forcemerge) { - policy.phases.warm.actions.forcemerge.index_codec = 'best_compression'; - } - } - - /** - * COLD PHASE SERIALIZATION - */ - if (policy.phases.cold) { - if (policy.phases.cold.min_age) { - policy.phases.cold.min_age = `${policy.phases.cold.min_age}${_meta.cold.minAgeUnit}`; - } - - policy.phases.cold.actions = serializeAllocateAction( - _meta.cold, - policy.phases.cold.actions, - originalPolicy?.phases.cold?.actions - ); - - if ( - policy.phases.cold.actions.allocate && - !policy.phases.cold.actions.allocate.require && - !isNumber(policy.phases.cold.actions.allocate.number_of_replicas) && - isEmpty(policy.phases.cold.actions.allocate.include) && - isEmpty(policy.phases.cold.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete policy.phases.cold.actions.allocate; - } - - if (_meta.cold.freezeEnabled) { - policy.phases.cold.actions.freeze = {}; - } - } - - /** - * DELETE PHASE SERIALIZATION - */ - if (policy.phases.delete) { - if (policy.phases.delete.min_age) { - policy.phases.delete.min_age = `${policy.phases.delete.min_age}${_meta.delete.minAgeUnit}`; - } - - if (originalPolicy?.phases.delete?.actions) { - const { wait_for_snapshot: __, ...rest } = originalPolicy.phases.delete.actions; - policy.phases.delete.actions = { - ...policy.phases.delete.actions, - ...rest, - }; - } - } - - return policy; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts new file mode 100644 index 0000000000000..f901bfcf4d49d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createSerializer } from './serializer'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts new file mode 100644 index 0000000000000..d18a63d34c101 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; + +import { SerializedActionWithAllocation } from '../../../../../../common/types'; + +import { DataAllocationMetaFields } from '../../types'; + +export const serializeMigrateAndAllocateActions = ( + { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, + newActions: SerializedActionWithAllocation = {}, + originalActions: SerializedActionWithAllocation = {} +): SerializedActionWithAllocation => { + const { allocate, migrate, ...otherActions } = newActions; + + // First copy over all non-allocate and migrate actions. + const actions: SerializedActionWithAllocation = { ...otherActions }; + + // The UI only knows about include, exclude and require, so copy over all other values. + if (allocate) { + const { include, exclude, require, ...otherSettings } = allocate; + if (!isEmpty(otherSettings)) { + actions.allocate = { ...otherSettings }; + } + } + + switch (dataTierAllocationType) { + case 'node_attrs': + if (allocationNodeAttribute) { + const [name, value] = allocationNodeAttribute.split(':'); + actions.allocate = { + // copy over any other allocate details like "number_of_replicas" + ...actions.allocate, + require: { + [name]: value, + }, + }; + } else { + // The form has been configured to use node attribute based allocation but no node attribute + // was selected. We fall back to what was originally selected in this case. This might be + // migrate.enabled: "false" + actions.migrate = originalActions.migrate; + } + + // copy over the original include and exclude values until we can set them in the form. + if (!isEmpty(originalActions?.allocate?.include)) { + actions.allocate = { + ...actions.allocate, + include: { ...originalActions?.allocate?.include }, + }; + } + + if (!isEmpty(originalActions?.allocate?.exclude)) { + actions.allocate = { + ...actions.allocate, + exclude: { ...originalActions?.allocate?.exclude }, + }; + } + break; + case 'none': + actions.migrate = { + ...originalActions?.migrate, + enabled: false, + }; + break; + default: + } + return actions; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts new file mode 100644 index 0000000000000..694f26abafe1d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { produce } from 'immer'; + +import { merge } from 'lodash'; + +import { SerializedPolicy } from '../../../../../../common/types'; + +import { defaultPolicy } from '../../../../constants'; + +import { FormInternal } from '../../types'; + +import { serializeMigrateAndAllocateActions } from './serialize_migrate_and_allocate_actions'; + +export const createSerializer = (originalPolicy?: SerializedPolicy) => ( + data: FormInternal +): SerializedPolicy => { + const { _meta, ...updatedPolicy } = data; + + if (!updatedPolicy.phases || !updatedPolicy.phases.hot) { + updatedPolicy.phases = { hot: { actions: {} } }; + } + + return produce<SerializedPolicy>(originalPolicy ?? defaultPolicy, (draft) => { + // Copy over all updated fields + merge(draft, updatedPolicy); + + // Next copy over all meta fields and delete any fields that have been removed + // by fields exposed in the form. It is very important that we do not delete + // data that the form does not control! E.g., unfollow action in hot phase. + + /** + * HOT PHASE SERIALIZATION + */ + if (draft.phases.hot) { + draft.phases.hot.min_age = draft.phases.hot.min_age ?? '0ms'; + } + + if (draft.phases.hot?.actions) { + const hotPhaseActions = draft.phases.hot.actions; + if (hotPhaseActions.rollover && _meta.hot.useRollover) { + if (hotPhaseActions.rollover.max_age) { + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.maxAgeUnit}`; + } + + if (hotPhaseActions.rollover.max_size) { + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + } + + if (!updatedPolicy.phases.hot!.actions?.forcemerge) { + delete hotPhaseActions.forcemerge; + } else if (_meta.hot.bestCompression) { + hotPhaseActions.forcemerge!.index_codec = 'best_compression'; + } + + if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { + hotPhaseActions.forcemerge.index_codec = 'best_compression'; + } + } else { + delete hotPhaseActions.rollover; + delete hotPhaseActions.forcemerge; + } + + if (!updatedPolicy.phases.hot!.actions?.set_priority) { + delete hotPhaseActions.set_priority; + } + } + + /** + * WARM PHASE SERIALIZATION + */ + if (_meta.warm.enabled) { + const warmPhase = draft.phases.warm!; + // If warm phase on rollover is enabled, delete min age field + // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + // They are mutually exclusive + if ( + (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && + updatedPolicy.phases.warm!.min_age + ) { + warmPhase.min_age = `${updatedPolicy.phases.warm!.min_age}${_meta.warm.minAgeUnit}`; + } else { + delete warmPhase.min_age; + } + + warmPhase.actions = serializeMigrateAndAllocateActions( + _meta.warm, + warmPhase.actions, + originalPolicy?.phases.warm?.actions + ); + + if (!updatedPolicy.phases.warm!.actions?.forcemerge) { + delete warmPhase.actions.forcemerge; + } else if (_meta.warm.bestCompression) { + warmPhase.actions.forcemerge!.index_codec = 'best_compression'; + } + + if (!updatedPolicy.phases.warm!.actions?.set_priority) { + delete warmPhase.actions.set_priority; + } + + if (!updatedPolicy.phases.warm!.actions?.shrink) { + delete warmPhase.actions.shrink; + } + } else { + delete draft.phases.warm; + } + + /** + * COLD PHASE SERIALIZATION + */ + if (_meta.cold.enabled) { + const coldPhase = draft.phases.cold!; + + if (updatedPolicy.phases.cold!.min_age) { + coldPhase.min_age = `${updatedPolicy.phases.cold!.min_age}${_meta.cold.minAgeUnit}`; + } + + coldPhase.actions = serializeMigrateAndAllocateActions( + _meta.cold, + coldPhase.actions, + originalPolicy?.phases.cold?.actions + ); + + if (_meta.cold.freezeEnabled) { + coldPhase.actions.freeze = coldPhase.actions.freeze ?? {}; + } else { + delete coldPhase.actions.freeze; + } + + if (!updatedPolicy.phases.cold!.actions?.set_priority) { + delete coldPhase.actions.set_priority; + } + } else { + delete draft.phases.cold; + } + + /** + * DELETE PHASE SERIALIZATION + */ + if (_meta.delete.enabled) { + const deletePhase = draft.phases.delete!; + if (updatedPolicy.phases.delete!.min_age) { + deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`; + } + + if ( + !updatedPolicy.phases.delete!.actions?.wait_for_snapshot && + deletePhase.actions.wait_for_snapshot + ) { + delete deletePhase.actions.wait_for_snapshot; + } + } else { + delete draft.phases.delete; + } + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index dc3d8a640e682..7d512936290af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -18,7 +18,6 @@ export interface MinAgeField { } export interface ForcemergeFields { - forceMergeEnabled: boolean; bestCompression: boolean; } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index 8b2140aa196b3..0943ced5e5be0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; -import { MetricsTab } from './tabs/metrics'; +import { MetricsTab } from './tabs/metrics/metrics'; import { LogsTab } from './tabs/logs'; import { ProcessesTab } from './tabs/processes'; import { PropertiesTab } from './tabs/properties'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx index 1a8bc374e79a3..ce800a7d73700 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx @@ -4,14 +4,86 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFieldSearch } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; import { TabContent, TabProps } from './shared'; +import { LogStream } from '../../../../../../components/log_stream'; +import { useWaffleOptionsContext } from '../../../hooks/use_waffle_options'; +import { findInventoryFields } from '../../../../../../../common/inventory_models'; +import { euiStyled } from '../../../../../../../../observability/public'; +import { useLinkProps } from '../../../../../../hooks/use_link_props'; +import { getNodeLogsUrl } from '../../../../../link_to'; const TabComponent = (props: TabProps) => { - return <TabContent>Logs Placeholder</TabContent>; + const [textQuery, setTextQuery] = useState(''); + const endTimestamp = props.currentTime; + const startTimestamp = endTimestamp - 60 * 60 * 1000; // 60 minutes + const { nodeType } = useWaffleOptionsContext(); + const { options, node } = props; + + const filter = useMemo(() => { + let query = options.fields + ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"` + : ``; + + if (textQuery) { + query += ` and message: ${textQuery}`; + } + return query; + }, [options, nodeType, node.id, textQuery]); + + const onQueryChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { + setTextQuery(e.target.value); + }, []); + + const nodeLogsMenuItemLinkProps = useLinkProps( + getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: startTimestamp, + }) + ); + + return ( + <TabContent> + <EuiFlexGroup gutterSize={'none'} alignItems="center"> + <EuiFlexItem> + <QueryWrapper> + <EuiFieldSearch + fullWidth + placeholder={i18n.translate('xpack.infra.nodeDetails.logs.textFieldPlaceholder', { + defaultMessage: 'Search for log entries...', + })} + value={textQuery} + isClearable + onChange={onQueryChange} + /> + </QueryWrapper> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType={'popout'} {...nodeLogsMenuItemLinkProps}> + <FormattedMessage + id="xpack.infra.nodeDetails.logs.openLogsLink" + defaultMessage="Open in Logs" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <LogStream startTimestamp={startTimestamp} endTimestamp={endTimestamp} query={filter} /> + </TabContent> + ); }; +const QueryWrapper = euiStyled.div` + padding: ${(props) => props.theme.eui.paddingSizes.m}; + padding-right: 0; +`; + export const LogsTab = { id: 'logs', name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', { diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 9ca6db40a3054..32812f19a2541 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -6,12 +6,7 @@ import { encode } from 'rison-node'; import { SearchResponse } from 'elasticsearch'; -import { - FetchData, - FetchDataParams, - HasData, - LogsFetchDataResponse, -} from '../../../observability/public'; +import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public'; import { DEFAULT_SOURCE_ID } from '../../common/constants'; import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; @@ -38,9 +33,7 @@ interface LogParams { type StatsAndSeries = Pick<LogsFetchDataResponse, 'stats' | 'series'>; -export function getLogsHasDataFetcher( - getStartServices: InfraClientCoreSetup['getStartServices'] -): HasData { +export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) { return async () => { const [core] = await getStartServices(); const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index b56ede1974393..14785f64cffac 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; @@ -18,6 +19,7 @@ import { buildErrorAlertReason, buildFiredAlertReason, buildNoDataAlertReason, + buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; @@ -56,6 +58,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = const inventoryItems = Object.keys(first(results)!); for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}`); + const prevState = alertInstance.getState(); // AND logic; all criteria must be across the threshold const shouldAlertFire = results.every((result) => // Grab the result of the most recent bucket @@ -80,6 +83,10 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = reason = results .map((result) => buildReasonWithVerboseMetricName(result[item], buildFiredAlertReason)) .join('\n'); + } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { + reason = results + .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) + .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { @@ -95,7 +102,9 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } } if (reason) { - alertInstance.scheduleActions(FIRED_ACTIONS.id, { + const actionGroupId = + nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + alertInstance.scheduleActions(actionGroupId, { group: item, alertState: stateToAlertMessage[nextState], reason, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 3a52bb6b6ce71..b31afba8ac9cc 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,6 +6,7 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, @@ -20,7 +21,7 @@ interface AlertTestInstance { state: any; } -let persistAlertInstances = false; // eslint-disable-line +let persistAlertInstances = false; describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { @@ -343,50 +344,49 @@ describe('The metric threshold alert type', () => { }); }); - // describe('querying a metric that later recovers', () => { - // const instanceID = '*'; - // const execute = (threshold: number[]) => - // executor({ - // - // services, - // params: { - // criteria: [ - // { - // ...baseCriterion, - // comparator: Comparator.GT, - // threshold, - // }, - // ], - // }, - // }); - // beforeAll(() => (persistAlertInstances = true)); - // afterAll(() => (persistAlertInstances = false)); + describe('querying a metric that later recovers', () => { + const instanceID = '*'; + const execute = (threshold: number[]) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator: Comparator.GT, + threshold, + }, + ], + }, + }); + beforeAll(() => (persistAlertInstances = true)); + afterAll(() => (persistAlertInstances = false)); - // test('sends a recovery alert as soon as the metric recovers', async () => { - // await execute([0.5]); - // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - // expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); - // await execute([2]); - // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - // expect(getState(instanceID).alertState).toBe(AlertStates.OK); - // }); - // test('does not continue to send a recovery alert if the metric is still OK', async () => { - // await execute([2]); - // expect(mostRecentAction(instanceID)).toBe(undefined); - // expect(getState(instanceID).alertState).toBe(AlertStates.OK); - // await execute([2]); - // expect(mostRecentAction(instanceID)).toBe(undefined); - // expect(getState(instanceID).alertState).toBe(AlertStates.OK); - // }); - // test('sends a recovery alert again once the metric alerts and recovers again', async () => { - // await execute([0.5]); - // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - // expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); - // await execute([2]); - // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - // expect(getState(instanceID).alertState).toBe(AlertStates.OK); - // }); - // }); + test('sends a recovery alert as soon as the metric recovers', async () => { + await execute([0.5]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute([2]); + expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + test('does not continue to send a recovery alert if the metric is still OK', async () => { + await execute([2]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + await execute([2]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + test('sends a recovery alert again once the metric alerts and recovers again', async () => { + await execute([0.5]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute([2]); + expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + }); describe('querying a metric with a percentage metric', () => { const instanceID = '*'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4dec552c5bd6c..7c3918c37ebbf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,12 +6,14 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraBackendLibs } from '../../infra_types'; import { buildErrorAlertReason, buildFiredAlertReason, buildNoDataAlertReason, + buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; import { createFormatter } from '../../../../common/formatters'; @@ -40,6 +42,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const groups = Object.keys(first(alertResults)!); for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${group}`); + const prevState = alertInstance.getState(); // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every((result) => @@ -64,6 +67,10 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => reason = alertResults .map((result) => buildFiredAlertReason(formatAlertResult(result[group]))) .join('\n'); + } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { + reason = alertResults + .map((result) => buildRecoveredAlertReason(formatAlertResult(result[group]))) + .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { @@ -81,7 +88,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => if (reason) { const firstResult = first(alertResults); const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); - alertInstance.scheduleActions(FIRED_ACTIONS.id, { + const actionGroupId = + nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], reason, @@ -98,7 +107,6 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => }); } - // Future use: ability to fetch display current alert state alertInstance.replaceState({ alertState: nextState, }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0af8e01d7290d..cf3752e649600 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -410,7 +410,7 @@ describe('Datatable Visualization', () => { const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame); - expect(error).not.toBeDefined(); + expect(error).toBeUndefined(); }); it('returns undefined if the metric dimension is defined', () => { @@ -427,7 +427,7 @@ describe('Datatable Visualization', () => { const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame); - expect(error).not.toBeDefined(); + expect(error).toBeUndefined(); }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 647c0f3ac9cca..0c96fc45de128 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -134,7 +134,7 @@ export const validateDatasourceAndVisualization = ( ? currentVisualization?.getErrorMessages(currentVisualizationState, frameAPI) : undefined; - if (datasourceValidationErrors || visualizationValidationErrors) { + if (datasourceValidationErrors?.length || visualizationValidationErrors?.length) { return [...(datasourceValidationErrors || []), ...(visualizationValidationErrors || [])]; } return undefined; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 00cb932a6d4e2..95aeedbd857ca 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -385,7 +385,7 @@ export const InnerVisualizationWrapper = ({ [dispatch] ); - if (localState.configurationValidationError) { + if (localState.configurationValidationError?.length) { let showExtraErrors = null; if (localState.configurationValidationError.length > 1) { if (localState.expandError) { @@ -445,7 +445,7 @@ export const InnerVisualizationWrapper = ({ ); } - if (localState.expressionBuildError) { + if (localState.expressionBuildError?.length) { return ( <EuiFlexGroup style={{ maxWidth: '100%' }} direction="column" alignItems="center"> <EuiFlexItem> 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 cd196745f3315..e5c05a1cf8c7a 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 @@ -419,7 +419,7 @@ export function DimensionEditor(props: DimensionEditorProps) { function getErrorMessage( selectedColumn: IndexPatternColumn | undefined, incompatibleSelectedOperationType: boolean, - input: 'none' | 'field' | undefined, + input: 'none' | 'field' | 'fullReference' | undefined, fieldInvalid: boolean ) { if (selectedColumn && incompatibleSelectedOperationType) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index b2edc61a56736..2e57ecee86033 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1054,6 +1054,7 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternId: '1', columns: {}, columnOrder: [], + incompleteColumns: {}, }, }, }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 31fb5277d53ec..817fdf637f001 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -21,6 +21,8 @@ type Props = Pick< 'layerId' | 'columnId' | 'state' | 'filterOperations' >; +// TODO: the support matrix should be available outside of the dimension panel + // TODO: This code has historically been memoized, as a potentially performance // sensitive task. If we can add memoization without breaking the behavior, we should. export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 51d95245adb25..3cf9bdc3a92f1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -13,9 +13,15 @@ import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; +import { + operationDefinitionMap, + getErrorMessages, + createMockedReferenceOperation, +} from './operations'; jest.mock('./loader'); jest.mock('../id_generator'); +jest.mock('./operations'); const fieldsOne = [ { @@ -489,6 +495,56 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); expect(ast.chain[0].arguments.timeFields).not.toContain('timefield'); }); + + describe('references', () => { + beforeEach(() => { + // @ts-expect-error we are inserting an invalid type + operationDefinitionMap.testReference = createMockedReferenceOperation(); + + // @ts-expect-error we are inserting an invalid type + operationDefinitionMap.testReference.toExpression.mockReturnValue(['mock']); + }); + + afterEach(() => { + delete operationDefinitionMap.testReference; + }); + + it('should collect expression references and append them', async () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count of records', + dataType: 'date', + isBucketed: false, + sourceField: 'timefield', + operationType: 'cardinality', + }, + col2: { + label: 'Reference', + dataType: 'number', + isBucketed: false, + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + // @ts-expect-error we can't isolate just the reference type + expect(operationDefinitionMap.testReference.toExpression).toHaveBeenCalled(); + expect(ast.chain[2]).toEqual('mock'); + }); + }); }); describe('#insertLayer', () => { @@ -599,11 +655,33 @@ describe('IndexPattern Data Source', () => { describe('getTableSpec', () => { it('should include col1', () => { - expect(publicAPI.getTableSpec()).toEqual([ - { - columnId: 'col1', + expect(publicAPI.getTableSpec()).toEqual([{ columnId: 'col1' }]); + }); + + it('should skip columns that are being referenced', () => { + publicAPI = indexPatternDatasource.getPublicAPI({ + state: { + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + // @ts-ignore this is too little information for a real column + col1: { + dataType: 'number', + }, + col2: { + // @ts-expect-error update once we have a reference operation outside tests + references: ['col1'], + }, + }, + }, + }, }, - ]); + layerId: 'first', + }); + + expect(publicAPI.getTableSpec()).toEqual([{ columnId: 'col2' }]); }); }); @@ -764,7 +842,7 @@ describe('IndexPattern Data Source', () => { dataType: 'number', isBucketed: false, label: 'Foo', - operationType: 'document', + operationType: 'avg', sourceField: 'bytes', }, }, @@ -774,7 +852,7 @@ describe('IndexPattern Data Source', () => { }; expect( indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState) - ).not.toBeDefined(); + ).toBeUndefined(); }); it('should return no errors with layers with no columns', () => { @@ -792,7 +870,31 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; - expect(indexPatternDatasource.getErrorMessages(state)).not.toBeDefined(); + expect(indexPatternDatasource.getErrorMessages(state)).toBeUndefined(); + }); + + it('should bubble up invalid configuration from operations', () => { + (getErrorMessages as jest.Mock).mockClear(); + (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, + currentIndexPatternId: '1', + }; + expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ + { shortMessage: 'error 1', longMessage: '' }, + { shortMessage: 'error 2', longMessage: '' }, + ]); + expect(getErrorMessages).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 94f240058d618..2c64431867df0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -40,13 +40,13 @@ import { } from './indexpattern_suggestions'; import { - getInvalidFieldReferencesForLayer, - getInvalidReferences, + getInvalidFieldsForLayer, + getInvalidLayers, isDraggedField, normalizeOperationDataType, } from './utils'; import { LayerPanel } from './layerpanel'; -import { IndexPatternColumn } from './operations'; +import { IndexPatternColumn, getErrorMessages } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -54,7 +54,7 @@ import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/pub import { mergeLayer } from './state_helpers'; import { Datasource, StateSetter } from '../index'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -import { deleteColumn } from './operations'; +import { deleteColumn, isReferenced } from './operations'; import { FieldBasedIndexPatternColumn } from './operations/definitions/column_types'; import { Dragging } from '../drag_drop/providers'; @@ -325,7 +325,9 @@ export function getIndexPatternDatasource({ datasourceId: 'indexpattern', getTableSpec: () => { - return state.layers[layerId].columnOrder.map((colId) => ({ columnId: colId })); + return state.layers[layerId].columnOrder + .filter((colId) => !isReferenced(state.layers[layerId], colId)) + .map((colId) => ({ columnId: colId })); }, getOperationForColumnId: (columnId: string) => { const layer = state.layers[layerId]; @@ -349,10 +351,17 @@ export function getIndexPatternDatasource({ if (!state) { return; } - const invalidLayers = getInvalidReferences(state); + const invalidLayers = getInvalidLayers(state); + + const layerErrors = Object.values(state.layers).flatMap((layer) => + (getErrorMessages(layer) ?? []).map((message) => ({ + shortMessage: message, + longMessage: '', + })) + ); if (invalidLayers.length === 0) { - return; + return layerErrors.length ? layerErrors : undefined; } const realIndex = Object.values(state.layers) @@ -363,64 +372,69 @@ export function getIndexPatternDatasource({ } }) .filter(Boolean) as Array<[number, number]>; - const invalidFieldsPerLayer: string[][] = getInvalidFieldReferencesForLayer( + const invalidFieldsPerLayer: string[][] = getInvalidFieldsForLayer( invalidLayers, state.indexPatterns ); const originalLayersList = Object.keys(state.layers); - return realIndex.map(([filteredIndex, layerIndex]) => { - const fieldsWithBrokenReferences: string[] = invalidFieldsPerLayer[filteredIndex].map( - (columnId) => { - const column = invalidLayers[filteredIndex].columns[ - columnId - ] as FieldBasedIndexPatternColumn; - return column.sourceField; - } - ); - - if (originalLayersList.length === 1) { - return { - shortMessage: i18n.translate( - 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', - { - defaultMessage: 'Invalid {fields, plural, one {reference} other {references}}.', + if (layerErrors.length || realIndex.length) { + return [ + ...layerErrors, + ...realIndex.map(([filteredIndex, layerIndex]) => { + const fieldsWithBrokenReferences: string[] = invalidFieldsPerLayer[filteredIndex].map( + (columnId) => { + const column = invalidLayers[filteredIndex].columns[ + columnId + ] as FieldBasedIndexPatternColumn; + return column.sourceField; + } + ); + + if (originalLayersList.length === 1) { + return { + shortMessage: i18n.translate( + 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', + { + defaultMessage: 'Invalid {fields, plural, one {reference} other {references}}.', + values: { + fields: fieldsWithBrokenReferences.length, + }, + } + ), + longMessage: i18n.translate( + 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', + { + defaultMessage: `{fieldsLength, plural, one {Field} other {Fields}} "{fields}" {fieldsLength, plural, one {has an} other {have}} invalid reference.`, + values: { + fields: fieldsWithBrokenReferences.join('", "'), + fieldsLength: fieldsWithBrokenReferences.length, + }, + } + ), + }; + } + return { + shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { + defaultMessage: + 'Invalid {fieldsLength, plural, one {reference} other {references}} on Layer {layer}.', values: { - fields: fieldsWithBrokenReferences.length, + layer: layerIndex, + fieldsLength: fieldsWithBrokenReferences.length, }, - } - ), - longMessage: i18n.translate( - 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', - { - defaultMessage: `{fieldsLength, plural, one {Field} other {Fields}} "{fields}" {fieldsLength, plural, one {has an} other {have}} invalid reference.`, + }), + longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { + defaultMessage: `Layer {layer} has {fieldsLength, plural, one {an invalid} other {invalid}} {fieldsLength, plural, one {reference} other {references}} in {fieldsLength, plural, one {field} other {fields}} "{fields}".`, values: { + layer: layerIndex, fields: fieldsWithBrokenReferences.join('", "'), fieldsLength: fieldsWithBrokenReferences.length, }, - } - ), - }; - } - return { - shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { - defaultMessage: - 'Invalid {fieldsLength, plural, one {reference} other {references}} on Layer {layer}.', - values: { - layer: layerIndex, - fieldsLength: fieldsWithBrokenReferences.length, - }, + }), + }; }), - longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { - defaultMessage: `Layer {layer} has {fieldsLength, plural, one {an invalid} other {invalid}} {fieldsLength, plural, one {reference} other {references}} in {fieldsLength, plural, one {field} other {fields}} "{fields}".`, - values: { - layer: layerIndex, - fields: fieldsWithBrokenReferences.join('", "'), - fieldsLength: fieldsWithBrokenReferences.length, - }, - }), - }; - }); + ]; + } }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index ccdefee62ad5c..263b4646c9feb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -18,7 +18,7 @@ import { IndexPatternColumn, OperationType, } from './operations'; -import { hasField, hasInvalidReference } from './utils'; +import { hasField, hasInvalidFields } from './utils'; import { IndexPattern, IndexPatternPrivateState, @@ -90,7 +90,7 @@ export function getDatasourceSuggestionsForField( indexPatternId: string, field: IndexPatternField ): IndexPatternSugestion[] { - if (hasInvalidReference(state)) return []; + if (hasInvalidFields(state)) return []; const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); @@ -331,7 +331,7 @@ function createNewLayerWithMetricAggregation( export function getDatasourceSuggestionsFromCurrentState( state: IndexPatternPrivateState ): Array<DatasourceSuggestion<IndexPatternPrivateState>> { - if (hasInvalidReference(state)) return []; + if (hasInvalidFields(state)) return []; const layers = Object.entries(state.layers || {}); if (layers.length > 1) { // Return suggestions that reduce the data to each layer individually diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 2c6f42668d863..d0cbcee61db6f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -6,7 +6,7 @@ import { DragContextState } from '../drag_drop'; import { getFieldByNameFactory } from './pure_helpers'; -import { IndexPattern } from './types'; +import type { IndexPattern } from './types'; export const createMockedIndexPattern = (): IndexPattern => { const fields = [ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 72dfe85dfc0e9..f27fb8d4642f6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -6,12 +6,14 @@ const actualOperations = jest.requireActual('../operations'); const actualHelpers = jest.requireActual('../layer_helpers'); +const actualMocks = jest.requireActual('../mocks'); jest.spyOn(actualOperations.operationDefinitionMap.date_histogram, 'paramEditor'); jest.spyOn(actualOperations.operationDefinitionMap.terms, 'onOtherColumnChanged'); jest.spyOn(actualHelpers, 'insertOrReplaceColumn'); jest.spyOn(actualHelpers, 'insertNewColumn'); jest.spyOn(actualHelpers, 'replaceColumn'); +jest.spyOn(actualHelpers, 'getErrorMessages'); export const { getAvailableOperationsByMetadata, @@ -35,4 +37,8 @@ export const { updateLayerIndexPattern, mergeLayer, isColumnTransferable, + getErrorMessages, + isReferenced, } = actualHelpers; + +export const { createMockedReferenceOperation } = actualMocks; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index bd8c4b4683396..fd3ca4319669e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -52,6 +52,8 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo (!newField.aggregationRestrictions || newField.aggregationRestrictions.cardinality) ); }, + getDefaultLabel: (column, indexPattern) => + ofName(indexPattern.getFieldByName(column.sourceField)!.displayName), buildColumn({ field, previousColumn }) { return { label: ofName(field.displayName), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index bd4b452a49e1d..13bddc0c2ec26 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Operation } from '../../../types'; +import type { Operation } from '../../../types'; -/** - * This is the root type of a column. If you are implementing a new - * operation, extend your column type on `BaseIndexPatternColumn` to make - * sure it's matching all the basic requirements. - */ export interface BaseIndexPatternColumn extends Operation { // Private operationType: string; @@ -18,7 +13,8 @@ export interface BaseIndexPatternColumn extends Operation { } // Formatting can optionally be added to any column -export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { +// export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { +export type FormattedIndexPatternColumn = BaseIndexPatternColumn & { params?: { format: { id: string; @@ -27,8 +23,20 @@ export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { }; }; }; -} +}; export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { sourceField: string; } + +export interface ReferenceBasedIndexPatternColumn + extends BaseIndexPatternColumn, + FormattedIndexPatternColumn { + references: string[]; +} + +// Used to store the temporary invalid state +export interface IncompleteColumn { + operationType?: string; + sourceField?: string; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index e33fc681b2579..30f64929fc1af 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -41,6 +41,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field }; } }, + getDefaultLabel: () => countLabel, buildColumn({ field, previousColumn }) { return { label: countLabel, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 7d50c28b7465a..558fab02ad084 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -188,7 +188,7 @@ describe('date_histogram', () => { describe('buildColumn', () => { it('should create column object with auto interval for primary time field', () => { const column = dateHistogramOperation.buildColumn({ - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, indexPattern: createMockedIndexPattern(), field: { name: 'timestamp', @@ -204,7 +204,7 @@ describe('date_histogram', () => { it('should create column object with auto interval for non-primary time fields', () => { const column = dateHistogramOperation.buildColumn({ - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, indexPattern: createMockedIndexPattern(), field: { name: 'start_date', @@ -220,7 +220,7 @@ describe('date_histogram', () => { it('should create column object with restrictions', () => { const column = dateHistogramOperation.buildColumn({ - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, indexPattern: createMockedIndexPattern(), field: { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 659390a42f261..efac9c151a435 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -59,6 +59,8 @@ export const dateHistogramOperation: OperationDefinition< }; } }, + getDefaultLabel: (column, indexPattern) => + indexPattern.getFieldByName(column.sourceField)!.displayName, buildColumn({ field }) { let interval = autoInterval; let timeZone: string | undefined; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 522e951bfba34..1b0452d18a79c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -75,6 +75,7 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n input: 'none', isTransferable: () => true, + getDefaultLabel: () => filtersLabel, buildColumn({ previousColumn }) { let params = { filters: [defaultFilter] }; if (previousColumn?.operationType === 'terms') { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 5c067ebaf21e9..0e7e125944e71 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ExpressionFunctionAST } from '@kbn/interpreter/common'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { termsOperation, TermsIndexPatternColumn } from './terms'; @@ -24,8 +25,13 @@ import { import { dateHistogramOperation, DateHistogramIndexPatternColumn } from './date_histogram'; import { countOperation, CountIndexPatternColumn } from './count'; import { StateSetter, OperationMetadata } from '../../../types'; -import { BaseIndexPatternColumn } from './column_types'; -import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types'; +import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; +import { + IndexPatternPrivateState, + IndexPattern, + IndexPatternField, + IndexPatternLayer, +} from '../../types'; import { DateRange } from '../../../../common'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { RangeIndexPatternColumn, rangeOperation } from './ranges'; @@ -50,6 +56,8 @@ export type IndexPatternColumn = export type FieldBasedIndexPatternColumn = Extract<IndexPatternColumn, { sourceField: string }>; +export { IncompleteColumn } from './column_types'; + // List of all operation definitions registered to this data source. // If you want to implement a new operation, add the definition to this array and // the column type to the `IndexPatternColumn` union type below. @@ -104,6 +112,14 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> { * Should be i18n-ified. */ displayName: string; + /** + * The default label is assigned by the editor + */ + getDefaultLabel: ( + column: C, + indexPattern: IndexPattern, + columns: Record<string, IndexPatternColumn> + ) => string; /** * This function is called if another column in the same layer changed or got removed. * Can be used to update references to other columns (e.g. for sorting). @@ -118,11 +134,6 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> { * React component for operation specific settings shown in the popover editor */ paramEditor?: React.ComponentType<ParamEditorProps<C>>; - /** - * Function turning a column into an agg config passed to the `esaggs` function - * together with the agg configs returned from other columns. - */ - toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; /** * Returns true if the `column` can also be used on `newIndexPattern`. * If this function returns false, the column is removed when switching index pattern @@ -138,7 +149,7 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> { } interface BaseBuildColumnArgs { - columns: Partial<Record<string, IndexPatternColumn>>; + layer: IndexPatternLayer; indexPattern: IndexPattern; } @@ -156,7 +167,12 @@ interface FieldlessOperationDefinition<C extends BaseIndexPatternColumn> { * Returns the meta data of the operation if applied. Undefined * if the field is not applicable. */ - getPossibleOperation: () => OperationMetadata | undefined; + getPossibleOperation: () => OperationMetadata; + /** + * Function turning a column into an agg config passed to the `esaggs` function + * together with the agg configs returned from other columns. + */ + toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; } interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> { @@ -167,7 +183,7 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> { */ getPossibleOperationForField: (field: IndexPatternField) => OperationMetadata | undefined; /** - * Builds the column object for the given parameters. Should include default p + * Builds the column object for the given parameters. */ buildColumn: ( arg: BaseBuildColumnArgs & { @@ -191,11 +207,76 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> { * @param field The field that the user changed to. */ onFieldChange: (oldColumn: C, field: IndexPatternField) => C; + /** + * Function turning a column into an agg config passed to the `esaggs` function + * together with the agg configs returned from other columns. + */ + toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; +} + +export interface RequiredReference { + // Limit the input types, usually used to prevent other references from being used + input: Array<GenericOperationDefinition['input']>; + // Function which is used to determine if the reference is bucketed, or if it's a number + validateMetadata: (metadata: OperationMetadata) => boolean; + // Do not use specificOperations unless you need to limit to only one or two exact + // operation types. The main use case is Cumulative Sum, where we need to only take the + // sum of Count or sum of Sum. + specificOperations?: OperationType[]; +} + +// Full reference uses one or more reference operations which are visible to the user +// Partial reference is similar except that it uses the field selector +interface FullReferenceOperationDefinition<C extends BaseIndexPatternColumn> { + input: 'fullReference'; + /** + * The filters provided here are used to construct the UI, transition correctly + * between operations, and validate the configuration. + */ + requiredReferences: RequiredReference[]; + + /** + * The type of UI that is shown in the editor for this function: + * - full: List of sub-functions and fields + * - field: List of fields, selects first operation per field + */ + selectionStyle: 'full' | 'field'; + + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + referenceIds: string[]; + previousColumn?: IndexPatternColumn; + } + ) => ReferenceBasedIndexPatternColumn & C; + /** + * Returns the meta data of the operation if applied. Undefined + * if the field is not applicable. + */ + getPossibleOperation: () => OperationMetadata; + /** + * A chain of expression functions which will transform the table + */ + toExpression: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern: IndexPattern + ) => ExpressionFunctionAST[]; + /** + * Validate that the operation has the right preconditions in the state. For example: + * + * - Requires a date histogram operation somewhere before it in order + * - Missing references + */ + getErrorMessage?: (layer: IndexPatternLayer, columnId: string) => string[] | undefined; } interface OperationDefinitionMap<C extends BaseIndexPatternColumn> { field: FieldBasedOperationDefinition<C>; none: FieldlessOperationDefinition<C>; + fullReference: FullReferenceOperationDefinition<C>; } /** @@ -220,7 +301,8 @@ export type OperationType = typeof internalOperationDefinitions[number]['type']; */ export type GenericOperationDefinition = | OperationDefinition<IndexPatternColumn, 'field'> - | OperationDefinition<IndexPatternColumn, 'none'>; + | OperationDefinition<IndexPatternColumn, 'none'> + | OperationDefinition<IndexPatternColumn, 'fullReference'>; /** * List of all available operation definitions diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 37a7ef8ee2563..96df72ba8b7c1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -52,6 +52,8 @@ function buildMetricOperation<T extends MetricColumn<string>>({ (!newField.aggregationRestrictions || newField.aggregationRestrictions![type]) ); }, + getDefaultLabel: (column, indexPattern, columns) => + ofName(indexPattern.getFieldByName(column.sourceField)!.displayName), buildColumn: ({ field, previousColumn }) => ({ label: ofName(field.displayName), dataType: 'number', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index b1cb2312d5bb8..d2456e1c8d375 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -122,9 +122,11 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field }; } }, + getDefaultLabel: (column, indexPattern) => + indexPattern.getFieldByName(column.sourceField)!.displayName, buildColumn({ field }) { return { - label: field.name, + label: field.displayName, dataType: 'number', // string for Range operationType: 'range', sourceField: field.name, 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 ddc473a5c588d..7c69a70c09351 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 @@ -17,7 +17,7 @@ import { EuiText, } from '@elastic/eui'; import { IndexPatternColumn } from '../../../indexpattern'; -import { updateColumnParam } from '../../layer_helpers'; +import { updateColumnParam, isReferenced } from '../../layer_helpers'; import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; @@ -82,13 +82,16 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field (!column.params.otherBucket || !newIndexPattern.hasRestrictions) ); }, - buildColumn({ columns, field, indexPattern }) { - const existingMetricColumn = Object.entries(columns) - .filter(([_columnId, column]) => column && isSortableByColumn(column)) + buildColumn({ layer, field, indexPattern }) { + const existingMetricColumn = Object.entries(layer.columns) + .filter( + ([columnId, column]) => column && !column.isBucketed && !isReferenced(layer, columnId) + ) .map(([id]) => id)[0]; - const previousBucketsLength = Object.values(columns).filter((col) => col && col.isBucketed) - .length; + const previousBucketsLength = Object.values(layer.columns).filter( + (col) => col && col.isBucketed + ).length; return { label: ofName(field.displayName), @@ -131,6 +134,8 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field }, }; }, + getDefaultLabel: (column, indexPattern) => + ofName(indexPattern.getFieldByName(column.sourceField)!.displayName), onFieldChange: (oldColumn, field) => { const newParams = { ...oldColumn.params }; if ('format' in newParams && field.type !== 'number') { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index bba7bda308b72..e43c7bbd2f72e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -270,7 +270,7 @@ describe('terms', () => { name: 'test', displayName: 'test', }, - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, }); expect(termsColumn.dataType).toEqual('boolean'); }); @@ -285,7 +285,7 @@ describe('terms', () => { name: 'test', displayName: 'test', }, - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, }); expect(termsColumn.params.otherBucket).toEqual(true); }); @@ -300,7 +300,7 @@ describe('terms', () => { name: 'test', displayName: 'test', }, - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, }); expect(termsColumn.params.otherBucket).toEqual(false); }); @@ -308,14 +308,18 @@ describe('terms', () => { it('should use existing metric column as order column', () => { const termsColumn = termsOperation.buildColumn({ indexPattern: createMockedIndexPattern(), - columns: { - col1: { - label: 'Count', - dataType: 'number', - isBucketed: false, - sourceField: 'Records', - operationType: 'count', + layer: { + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, }, + columnOrder: [], + indexPatternId: '', }, field: { aggregatable: true, @@ -335,7 +339,7 @@ describe('terms', () => { it('should use the default size when there is an existing bucket', () => { const termsColumn = termsOperation.buildColumn({ indexPattern: createMockedIndexPattern(), - columns: state.layers.first.columns, + layer: state.layers.first, field: { aggregatable: true, searchable: true, @@ -350,7 +354,7 @@ describe('terms', () => { it('should use a size of 5 when there are no other buckets', () => { const termsColumn = termsOperation.buildColumn({ indexPattern: createMockedIndexPattern(), - columns: {}, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, field: { aggregatable: true, searchable: true, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts index f0e02c7ff0faf..3ad9a1e5b3674 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts @@ -6,4 +6,11 @@ export * from './operations'; export * from './layer_helpers'; -export { OperationType, IndexPatternColumn, FieldBasedIndexPatternColumn } from './definitions'; +export { + OperationType, + IndexPatternColumn, + FieldBasedIndexPatternColumn, + IncompleteColumn, +} from './definitions'; + +export { createMockedReferenceOperation } from './mocks'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index e1a31dc274837..0d103a766c23a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import type { OperationMetadata } from '../../types'; import { insertNewColumn, replaceColumn, @@ -11,16 +12,20 @@ import { getColumnOrder, deleteColumn, updateLayerIndexPattern, + getErrorMessages, } from './layer_helpers'; import { operationDefinitionMap, OperationType } from '../operations'; import { TermsIndexPatternColumn } from './definitions/terms'; import { DateHistogramIndexPatternColumn } from './definitions/date_histogram'; import { AvgIndexPatternColumn } from './definitions/metrics'; -import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from '../types'; +import type { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from '../types'; import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; +import { generateId } from '../../id_generator'; +import { createMockedReferenceOperation } from './mocks'; jest.mock('../operations'); +jest.mock('../../id_generator'); const indexPatternFields = [ { @@ -74,10 +79,22 @@ const indexPattern = { timeFieldName: 'timestamp', hasRestrictions: false, fields: indexPatternFields, - getFieldByName: getFieldByNameFactory(indexPatternFields), + getFieldByName: getFieldByNameFactory([...indexPatternFields, documentField]), }; describe('state_helpers', () => { + beforeEach(() => { + let count = 0; + (generateId as jest.Mock).mockImplementation(() => `id${++count}`); + + // @ts-expect-error we are inserting an invalid type + operationDefinitionMap.testReference = createMockedReferenceOperation(); + }); + + afterEach(() => { + delete operationDefinitionMap.testReference; + }); + describe('insertNewColumn', () => { it('should throw for invalid operations', () => { expect(() => { @@ -315,6 +332,110 @@ describe('state_helpers', () => { }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); + + describe('inserting a new reference', () => { + it('should throw if the required references are impossible to match', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['none', 'field'], + validateMetadata: () => false, + specificOperations: [], + }, + ]; + const layer: IndexPatternLayer = { indexPatternId: '1', columnOrder: [], columns: {} }; + expect(() => { + insertNewColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'testReference' as OperationType, + }); + }).toThrow(); + }); + + it('should leave the references empty if too ambiguous', () => { + const layer: IndexPatternLayer = { indexPatternId: '1', columnOrder: [], columns: {} }; + const result = insertNewColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'testReference' as OperationType, + }); + + expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( + expect.objectContaining({ + referenceIds: ['id1'], + }) + ); + expect(result).toEqual( + expect.objectContaining({ + columns: { + col2: expect.objectContaining({ references: ['id1'] }), + }, + }) + ); + }); + + it('should create an operation if there is exactly one possible match', () => { + // There is only one operation with `none` as the input type + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['none'], + validateMetadata: () => true, + }, + ]; + const layer: IndexPatternLayer = { indexPatternId: '1', columnOrder: [], columns: {} }; + const result = insertNewColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error invalid type + op: 'testReference', + }); + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual( + expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'filters' }), + col1: expect.objectContaining({ references: ['id1'] }), + }) + ); + }); + + it('should create a referenced column if the ID is being used as a reference', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + + // @ts-expect-error only in test + operationType: 'testReference', + references: ['ref1'], + }, + }, + }; + expect( + insertNewColumn({ + layer, + indexPattern, + columnId: 'ref1', + op: 'count', + field: documentField, + }) + ).toEqual( + expect.objectContaining({ + columns: { + col1: expect.objectContaining({ references: ['ref1'] }), + ref1: expect.objectContaining({}), + }, + }) + ); + }); + }); }); describe('replaceColumn', () => { @@ -655,10 +776,301 @@ describe('state_helpers', () => { }), }); }); + + it('should not wrap the previous operation when switching to reference', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + op: 'testReference' as OperationType, + }); + + expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( + expect.objectContaining({ + referenceIds: ['id1'], + }) + ); + expect(result.columns).toEqual( + expect.objectContaining({ + col1: expect.objectContaining({ operationType: 'testReference' }), + }) + ); + }); + + it('should delete the previous references and reset to default values when going from reference to no-input', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['none'], + validateMetadata: () => true, + }, + ]; + const expectedCol = { + dataType: 'string' as const, + isBucketed: true, + + operationType: 'filters' as const, + params: { + // These filters are reset + filters: [{ input: { query: 'field: true', language: 'kuery' }, label: 'Custom label' }], + }, + }; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + ...expectedCol, + label: 'Custom label', + customLabel: true, + }, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'filters', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: { + ...expectedCol, + label: 'Filters', + scale: 'ordinal', // added in buildColumn + params: { + filters: [{ input: { query: '', language: 'kuery' }, label: '' }], + }, + }, + }, + }) + ); + }); + + it('should delete the inner references when switching away from reference to field-based operation', () => { + const expectedCol = { + label: 'Count of records', + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: expectedCol, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'count', + field: documentField, + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: expect.objectContaining(expectedCol), + }, + }) + ); + }); + + it('should reset when switching from one reference to another', () => { + operationDefinitionMap.secondTest = { + input: 'fullReference', + displayName: 'Reference test 2', + // @ts-expect-error this type is not statically available + type: 'secondTest', + requiredReferences: [ + { + // Any numeric metric that isn't also a reference + input: ['none', 'field'], + validateMetadata: (meta: OperationMetadata) => + meta.dataType === 'number' && !meta.isBucketed, + }, + ], + // @ts-expect-error don't want to define valid arguments + buildColumn: jest.fn((args) => { + return { + label: 'Test reference', + isBucketed: false, + dataType: 'number', + + operationType: 'secondTest', + references: args.referenceIds, + }; + }), + isTransferable: jest.fn(), + toExpression: jest.fn().mockReturnValue([]), + getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), + }; + + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: expect.objectContaining({ references: ['id1'] }), + }, + incompleteColumns: {}, + }) + ); + + delete operationDefinitionMap.secondTest; + }); + + it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number', + specificOperations: ['sum'], + }, + ]; + + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Asdf', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + + operationType: 'sum' as const, + sourceField: 'bytes', + }, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + op: 'count', + field: documentField, + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + sourceField: 'Records', + operationType: 'count', + }), + col2: expect.objectContaining({ references: ['col1'] }), + }, + }) + ); + }); }); describe('deleteColumn', () => { - it('should remove column', () => { + it('should clear incomplete columns when column is already empty', () => { + expect( + deleteColumn({ + layer: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { + col1: { sourceField: 'test' }, + }, + }, + columnId: 'col1', + }) + ).toEqual({ + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: {}, + }); + }); + + it('should remove column and any incomplete state', () => { const termsColumn: TermsIndexPatternColumn = { label: 'Top values of source', dataType: 'string', @@ -682,25 +1094,33 @@ describe('state_helpers', () => { columns: { col1: termsColumn, col2: { - label: 'Count', + label: 'Count of records', dataType: 'number', isBucketed: false, sourceField: 'Records', operationType: 'count', }, }, + incompleteColumns: { + col2: { sourceField: 'other' }, + }, }, columnId: 'col2', - }).columns + }) ).toEqual({ - col1: { - ...termsColumn, - params: { - ...termsColumn.params, - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + ...termsColumn, + params: { + ...termsColumn.params, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, }, }, + incompleteColumns: {}, }); }); @@ -742,6 +1162,73 @@ describe('state_helpers', () => { col1: termsColumn, }); }); + + it('should delete the column and all of its references', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + operationType: 'count', + sourceField: 'Records', + }, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect(deleteColumn({ layer, columnId: 'col2' })).toEqual( + expect.objectContaining({ columnOrder: [], columns: {} }) + ); + }); + + it('should recursively delete references', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + operationType: 'count', + sourceField: 'Records', + }, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + col3: { + label: 'Test reference 2', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col2'], + }, + }, + }; + expect(deleteColumn({ layer, columnId: 'col3' })).toEqual( + expect.objectContaining({ columnOrder: [], columns: {} }) + ); + }); }); describe('updateColumnParam', () => { @@ -913,6 +1400,60 @@ describe('state_helpers', () => { }) ).toEqual(['col1', 'col3', 'col2']); }); + + it('should correctly sort references to other references', () => { + expect( + getColumnOrder({ + columnOrder: [], + indexPatternId: '', + columns: { + bucket: { + label: 'Top values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + }, + }, + metric: { + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + }, + ref2: { + label: 'Ref2', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error only for testing + operationType: 'testReference', + references: ['ref1'], + }, + ref1: { + label: 'Ref', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error only for testing + operationType: 'testReference', + references: ['bucket'], + }, + }, + }) + ).toEqual(['bucket', 'metric', 'ref1', 'ref2']); + }); }); describe('updateLayerIndexPattern', () => { @@ -1141,4 +1682,67 @@ describe('state_helpers', () => { }); }); }); + + describe('getErrorMessages', () => { + it('should collect errors from the operation definitions', () => { + const mock = jest.fn().mockReturnValue(['error 1']); + // @ts-expect-error not statically analyzed + operationDefinitionMap.testReference.getErrorMessage = mock; + const errors = getErrorMessages({ + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed + { operationType: 'testReference', references: [] }, + }, + }); + expect(mock).toHaveBeenCalled(); + expect(errors).toHaveLength(1); + }); + + it('should identify missing references', () => { + const errors = getErrorMessages({ + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed yet + { operationType: 'testReference', references: ['ref1', 'ref2'] }, + }, + }); + expect(errors).toHaveLength(2); + }); + + it('should identify references that are no longer valid', () => { + // There is only one operation with `none` as the input type + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['none'], + validateMetadata: () => true, + }, + ]; + + const errors = getErrorMessages({ + indexPatternId: '1', + columnOrder: [], + columns: { + // @ts-expect-error incomplete operation + ref1: { + dataType: 'string', + isBucketed: true, + operationType: 'terms', + }, + col1: { + label: '', + references: ['ref1'], + // @ts-expect-error tests only + operationType: 'testReference', + }, + }, + }); + expect(errors).toHaveLength(1); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index f071df1542147..1495a876a2c8e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -5,13 +5,15 @@ */ import _, { partition } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { operationDefinitionMap, operationDefinitions, OperationType, IndexPatternColumn, + RequiredReference, } from './definitions'; -import { +import type { IndexPattern, IndexPatternField, IndexPatternLayer, @@ -19,6 +21,7 @@ import { } from '../types'; import { getSortScoreByPriority } from './operations'; import { mergeLayer } from '../state_helpers'; +import { generateId } from '../../id_generator'; interface ColumnChange { op: OperationType; @@ -35,6 +38,8 @@ export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { return insertNewColumn(args); } +// Insert a column into an empty ID. The field parameter is required when constructing +// a field-based operation, but will cause the function to fail for any other type of operation. export function insertNewColumn({ op, layer, @@ -48,24 +53,102 @@ export function insertNewColumn({ throw new Error('No suitable operation found for given parameters'); } - const baseOptions = { - columns: layer.columns, - indexPattern, - previousColumn: layer.columns[columnId], - }; + if (layer.columns[columnId]) { + throw new Error(`Can't insert a column with an ID that is already in use`); + } - // TODO: Reference based operations require more setup to create the references + const baseOptions = { indexPattern, previousColumn: layer.columns[columnId] }; if (operationDefinition.input === 'none') { + if (field) { + throw new Error(`Can't create operation ${op} with the provided field ${field.name}`); + } const possibleOperation = operationDefinition.getPossibleOperation(); - if (!possibleOperation) { - throw new Error('Tried to create an invalid operation'); + const isBucketed = Boolean(possibleOperation.isBucketed); + if (isBucketed) { + return addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId); + } else { + return addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId); } + } + + if (operationDefinition.input === 'fullReference') { + if (field) { + throw new Error(`Reference-based operations can't take a field as input when creating`); + } + let tempLayer = { ...layer }; + const referenceIds = operationDefinition.requiredReferences.map((validation) => { + // TODO: This logic is too simple because it's not using fields. Once we have + // access to the operationSupportMatrix, we should validate the metadata against + // the possible fields + const validOperations = Object.values(operationDefinitionMap).filter(({ type }) => + isOperationAllowedAsReference({ validation, operationType: type }) + ); + + if (!validOperations.length) { + throw new Error( + `Can't create reference, ${op} has a validation function which doesn't allow any operations` + ); + } + + const newId = generateId(); + if (validOperations.length === 1) { + const def = validOperations[0]; + + const validFields = + def.input === 'field' ? indexPattern.fields.filter(def.getPossibleOperationForField) : []; + + if (def.input === 'none') { + tempLayer = insertNewColumn({ + layer: tempLayer, + columnId: newId, + op: def.type, + indexPattern, + }); + } else if (validFields.length === 1) { + // Recursively update the layer for each new reference + tempLayer = insertNewColumn({ + layer: tempLayer, + columnId: newId, + op: def.type, + indexPattern, + field: validFields[0], + }); + } else { + tempLayer = { + ...tempLayer, + incompleteColumns: { + ...tempLayer.incompleteColumns, + [newId]: { operationType: def.type }, + }, + }; + } + } + return newId; + }); + + const possibleOperation = operationDefinition.getPossibleOperation(); const isBucketed = Boolean(possibleOperation.isBucketed); if (isBucketed) { - return addBucket(layer, operationDefinition.buildColumn(baseOptions), columnId); + return addBucket( + tempLayer, + operationDefinition.buildColumn({ + ...baseOptions, + layer: tempLayer, + referenceIds, + }), + columnId + ); } else { - return addMetric(layer, operationDefinition.buildColumn(baseOptions), columnId); + return addMetric( + tempLayer, + operationDefinition.buildColumn({ + ...baseOptions, + layer: tempLayer, + referenceIds, + }), + columnId + ); } } @@ -81,9 +164,17 @@ export function insertNewColumn({ } const isBucketed = Boolean(possibleOperation.isBucketed); if (isBucketed) { - return addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, field }), columnId); + return addBucket( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field }), + columnId + ); } else { - return addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, field }), columnId); + return addMetric( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field }), + columnId + ); } } @@ -99,8 +190,9 @@ export function replaceColumn({ throw new Error(`Can't replace column because there is no prior column`); } - const isNewOperation = Boolean(op) && op !== previousColumn.operationType; - const operationDefinition = operationDefinitionMap[op || previousColumn.operationType]; + const isNewOperation = op !== previousColumn.operationType; + const operationDefinition = operationDefinitionMap[op]; + const previousDefinition = operationDefinitionMap[previousColumn.operationType]; if (!operationDefinition) { throw new Error('No suitable operation found for given parameters'); @@ -113,22 +205,49 @@ export function replaceColumn({ }; if (isNewOperation) { - // TODO: Reference based operations require more setup to create the references + let tempLayer = { ...layer }; - if (operationDefinition.input === 'none') { - const newColumn = operationDefinition.buildColumn(baseOptions); + if (previousDefinition.input === 'fullReference') { + // @ts-expect-error references are not statically analyzed + previousColumn.references.forEach((id: string) => { + tempLayer = deleteColumn({ layer: tempLayer, columnId: id }); + }); + } + if (operationDefinition.input === 'fullReference') { + const referenceIds = operationDefinition.requiredReferences.map(() => generateId()); + + const incompleteColumns = { ...(tempLayer.incompleteColumns || {}) }; + delete incompleteColumns[columnId]; + const newColumns = { + ...tempLayer.columns, + [columnId]: operationDefinition.buildColumn({ + ...baseOptions, + layer: tempLayer, + referenceIds, + previousColumn, + }), + }; + return { + ...tempLayer, + columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), + columns: newColumns, + incompleteColumns, + }; + } + + if (operationDefinition.input === 'none') { + const newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer }); if (previousColumn.customLabel) { newColumn.customLabel = true; newColumn.label = previousColumn.label; } + const newColumns = { ...tempLayer.columns, [columnId]: newColumn }; return { - ...layer, - columns: adjustColumnReferencesForChangedColumn( - { ...layer.columns, [columnId]: newColumn }, - columnId - ), + ...tempLayer, + columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), + columns: adjustColumnReferencesForChangedColumn(newColumns, columnId), }; } @@ -136,17 +255,17 @@ export function replaceColumn({ throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); } - const newColumn = operationDefinition.buildColumn({ ...baseOptions, field }); + const newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); if (previousColumn.customLabel) { newColumn.customLabel = true; newColumn.label = previousColumn.label; } - const newColumns = { ...layer.columns, [columnId]: newColumn }; + const newColumns = { ...tempLayer.columns, [columnId]: newColumn }; return { - ...layer, - columnOrder: getColumnOrder({ ...layer, columns: newColumns }), + ...tempLayer, + columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), columns: adjustColumnReferencesForChangedColumn(newColumns, columnId), }; } else if ( @@ -294,23 +413,61 @@ export function deleteColumn({ layer: IndexPatternLayer; columnId: string; }): IndexPatternLayer { + const column = layer.columns[columnId]; + if (!column) { + const newIncomplete = { ...(layer.incompleteColumns || {}) }; + delete newIncomplete[columnId]; + return { + ...layer, + columnOrder: layer.columnOrder.filter((id) => id !== columnId), + incompleteColumns: newIncomplete, + }; + } + + // @ts-expect-error this fails statically because there are no references added + const extraDeletions: string[] = 'references' in column ? column.references : []; + const hypotheticalColumns = { ...layer.columns }; delete hypotheticalColumns[columnId]; - const newLayer = { + let newLayer = { ...layer, columns: adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId), }; - return { ...newLayer, columnOrder: getColumnOrder(newLayer) }; + + extraDeletions.forEach((id) => { + newLayer = deleteColumn({ layer: newLayer, columnId: id }); + }); + + const newIncomplete = { ...(newLayer.incompleteColumns || {}) }; + delete newIncomplete[columnId]; + + return { ...newLayer, columnOrder: getColumnOrder(newLayer), incompleteColumns: newIncomplete }; } export function getColumnOrder(layer: IndexPatternLayer): string[] { - const [aggregations, metrics] = _.partition( + const [direct, referenceBased] = _.partition( Object.entries(layer.columns), - ([id, col]) => col.isBucketed + ([id, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference' ); + // If a reference has another reference as input, put it last in sort order + referenceBased.sort(([idA, a], [idB, b]) => { + // @ts-expect-error not statically analyzed + if ('references' in a && a.references.includes(idB)) { + return 1; + } + // @ts-expect-error not statically analyzed + if ('references' in b && b.references.includes(idA)) { + return -1; + } + return 0; + }); + const [aggregations, metrics] = _.partition(direct, ([, col]) => col.isBucketed); - return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id)); + return aggregations + .map(([id]) => id) + .concat(metrics.map(([id]) => id)) + .concat(referenceBased.map(([id]) => id)); } /** @@ -342,3 +499,116 @@ export function updateLayerIndexPattern( columnOrder: newColumnOrder, }; } + +/** + * Collects all errors from the columns in the layer, for display in the workspace. This includes: + * + * - All columns have complete references + * - All column references are valid + * - All prerequisites are met + */ +export function getErrorMessages(layer: IndexPatternLayer): string[] | undefined { + const errors: string[] = []; + + Object.entries(layer.columns).forEach(([columnId, column]) => { + const def = operationDefinitionMap[column.operationType]; + if (def.input === 'fullReference' && def.getErrorMessage) { + errors.push(...(def.getErrorMessage(layer, columnId) ?? [])); + } + + if ('references' in column) { + // @ts-expect-error references are not statically analyzed yet + column.references.forEach((referenceId, index) => { + if (!layer.columns[referenceId]) { + errors.push( + i18n.translate('xpack.lens.indexPattern.missingReferenceError', { + defaultMessage: 'Dimension {dimensionLabel} is incomplete', + values: { + // @ts-expect-error references are not statically analyzed yet + dimensionLabel: column.label, + }, + }) + ); + } else { + const referenceColumn = layer.columns[referenceId]!; + const requirements = + // @ts-expect-error not statically analyzed + operationDefinitionMap[column.operationType].requiredReferences[index]; + const isValid = isColumnValidAsReference({ + validation: requirements, + column: referenceColumn, + }); + + if (!isValid) { + errors.push( + i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', { + defaultMessage: 'Dimension {dimensionLabel} does not have a valid configuration', + values: { + // @ts-expect-error references are not statically analyzed yet + dimensionLabel: column.label, + }, + }) + ); + } + } + }); + } + }); + + return errors.length ? errors : undefined; +} + +export function isReferenced(layer: IndexPatternLayer, columnId: string): boolean { + const allReferences = Object.values(layer.columns).flatMap((col) => + 'references' in col + ? // @ts-expect-error not statically analyzed + col.references + : [] + ); + return allReferences.includes(columnId); +} + +function isColumnValidAsReference({ + column, + validation, +}: { + column: IndexPatternColumn; + validation: RequiredReference; +}): boolean { + if (!column) return false; + const operationType = column.operationType; + const operationDefinition = operationDefinitionMap[operationType]; + return ( + validation.input.includes(operationDefinition.input) && + (!validation.specificOperations || validation.specificOperations.includes(operationType)) && + validation.validateMetadata(column) + ); +} + +function isOperationAllowedAsReference({ + operationType, + validation, + field, +}: { + operationType: OperationType; + validation: RequiredReference; + field?: IndexPatternField; +}): boolean { + const operationDefinition = operationDefinitionMap[operationType]; + + let hasValidMetadata = true; + if (field && operationDefinition.input === 'field') { + const metadata = operationDefinition.getPossibleOperationForField(field); + hasValidMetadata = Boolean(metadata) && validation.validateMetadata(metadata!); + } else if (operationDefinition.input !== 'field') { + const metadata = operationDefinition.getPossibleOperation(); + hasValidMetadata = Boolean(metadata) && validation.validateMetadata(metadata!); + } else { + // TODO: How can we validate the metadata without a specific field? + } + return ( + validation.input.includes(operationDefinition.input) && + (!validation.specificOperations || validation.specificOperations.includes(operationType)) && + hasValidMetadata + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts new file mode 100644 index 0000000000000..c3f7dac03ada3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { OperationMetadata } from '../../types'; +import type { OperationType } from './definitions'; + +export const createMockedReferenceOperation = () => { + return { + input: 'fullReference', + displayName: 'Reference test', + type: 'testReference' as OperationType, + selectionStyle: 'full', + requiredReferences: [ + { + // Any numeric metric that isn't also a reference + input: ['none', 'field'], + validateMetadata: (meta: OperationMetadata) => + meta.dataType === 'number' && !meta.isBucketed, + }, + ], + buildColumn: jest.fn((args) => { + return { + label: 'Test reference', + isBucketed: false, + dataType: 'number', + + operationType: 'testReference', + references: args.referenceIds, + }; + }), + isTransferable: jest.fn(), + toExpression: jest.fn().mockReturnValue([]), + getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: jest.fn().mockReturnValue('Default label'), + }; +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 8d489df366088..58685fa494a04 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -87,6 +87,10 @@ type OperationFieldTuple = | { type: 'none'; operationType: OperationType; + } + | { + type: 'fullReference'; + operationType: OperationType; }; /** @@ -162,6 +166,11 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { }, operationDefinition.getPossibleOperation() ); + } else if (operationDefinition.input === 'fullReference') { + addToMap( + { type: 'fullReference', operationType: operationDefinition.type }, + operationDefinition.getPossibleOperation() + ); } }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index ea7aa62054e5c..5b66d4aae77ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -7,32 +7,29 @@ import { Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; -import { IndexPattern, IndexPatternPrivateState } from './types'; +import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; import { OriginalColumn } from './rename_columns'; import { dateHistogramOperation } from './operations/definitions'; -function getExpressionForLayer( - indexPattern: IndexPattern, - columns: Record<string, IndexPatternColumn>, - columnOrder: string[] -): Ast | null { +function getExpressionForLayer(layer: IndexPatternLayer, indexPattern: IndexPattern): Ast | null { + const { columns, columnOrder } = layer; + if (columnOrder.length === 0) { return null; } - function getEsAggsConfig<C extends IndexPatternColumn>(column: C, columnId: string) { - return operationDefinitionMap[column.operationType].toEsAggsConfig( - column, - columnId, - indexPattern - ); - } - const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const); if (columnEntries.length) { - const aggs = columnEntries.map(([colId, col]) => { - return getEsAggsConfig(col, colId); + const aggs: unknown[] = []; + const expressions: ExpressionFunctionAST[] = []; + columnEntries.forEach(([colId, col]) => { + const def = operationDefinitionMap[col.operationType]; + if (def.input === 'fullReference') { + expressions.push(...def.toExpression(layer, colId, indexPattern)); + } else { + aggs.push(def.toEsAggsConfig(col, colId, indexPattern)); + } }); const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { @@ -119,6 +116,7 @@ function getExpressionForLayer( }, }, ...formatterOverrides, + ...expressions, ], }; } @@ -129,9 +127,8 @@ function getExpressionForLayer( export function toExpression(state: IndexPatternPrivateState, layerId: string) { if (state.layers[layerId]) { return getExpressionForLayer( - state.indexPatterns[state.layers[layerId].indexPatternId], - state.layers[layerId].columns, - state.layers[layerId].columnOrder + state.layers[layerId], + state.indexPatterns[state.layers[layerId].indexPatternId] ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 1e6fc5a5806b5..e4958da471417 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -5,7 +5,7 @@ */ import { IFieldType } from 'src/plugins/data/common'; -import { IndexPatternColumn } from './operations'; +import { IndexPatternColumn, IncompleteColumn } from './operations'; import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public'; export interface IndexPattern { @@ -35,6 +35,8 @@ export interface IndexPatternLayer { columns: Record<string, IndexPatternColumn>; // Each layer is tied to the index pattern that created it indexPatternId: string; + // Partial columns represent the temporary invalid states + incompleteColumns?: Record<string, IncompleteColumn>; } export interface IndexPatternPersistedState { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index d0ea81d135156..01b834610eb1a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -42,11 +42,11 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg ); } -export function hasInvalidReference(state: IndexPatternPrivateState) { - return getInvalidReferences(state).length > 0; +export function hasInvalidFields(state: IndexPatternPrivateState) { + return getInvalidLayers(state).length > 0; } -export function getInvalidReferences(state: IndexPatternPrivateState) { +export function getInvalidLayers(state: IndexPatternPrivateState) { return Object.values(state.layers).filter((layer) => { return layer.columnOrder.some((columnId) => { const column = layer.columns[columnId]; @@ -62,7 +62,7 @@ export function getInvalidReferences(state: IndexPatternPrivateState) { }); } -export function getInvalidFieldReferencesForLayer( +export function getInvalidFieldsForLayer( layers: IndexPatternLayer[], indexPatternMap: Record<string, IndexPattern> ) { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index a4c1e1bd4ba16..a4b5d741c80f1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -18,7 +18,7 @@ import { Fit, } from '@elastic/charts'; import { PaletteOutput } from 'src/plugins/charts/public'; -import { xyChart, XYChart } from './expression'; +import { calculateMinInterval, xyChart, XYChart, XYChartProps } from './expression'; import { LensMultiTable } from '../types'; import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public'; import React from 'react'; @@ -287,6 +287,10 @@ function sampleArgs() { { a: 1, b: 5, c: 'J', d: 'Bar' }, ]), }, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, }; const args: XYArgs = createArgsWithLayers(); @@ -425,7 +429,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -449,7 +453,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -502,7 +506,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={undefined} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -516,7 +520,7 @@ describe('xy_expression', () => { `); }); - test('it generates correct xDomain for a layer with single value and a layer with no data (1-0) ', () => { + test('it uses passed in minInterval', () => { const data: LensMultiTable = { type: 'lens_multitable', tables: { @@ -539,7 +543,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -550,132 +554,10 @@ describe('xy_expression', () => { Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 1728000, + "minInterval": 50, } `); }); - - test('it generates correct xDomain for two layers with single value(1-1)', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), - second: createSampleDatatableWithRows([{ a: 10, b: 5, c: 'J', d: 'Bar' }]), - }, - }; - const component = shallow( - <XYChart - data={{ - ...data, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), - }, - }} - args={multiLayerArgs} - formatFactory={getFormatSpy} - timeZone="UTC" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - histogramBarTarget={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} - /> - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); - test('it generates correct xDomain for a layer with single value and layer with multiple value data (1-n)', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), - second: createSampleDatatableWithRows([ - { a: 10, b: 5, c: 'J', d: 'Bar' }, - { a: 8, b: 5, c: 'K', d: 'Buzz' }, - ]), - }, - }; - const component = shallow( - <XYChart - data={{ - ...data, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), - }, - }} - args={multiLayerArgs} - formatFactory={getFormatSpy} - timeZone="UTC" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - histogramBarTarget={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} - /> - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); - - test('it generates correct xDomain for 2 layers with multiple value data (n-n)', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: 'I', d: 'Foo' }, - { a: 8, b: 5, c: 'K', d: 'Buzz' }, - { a: 9, b: 7, c: 'L', d: 'Bar' }, - { a: 10, b: 2, c: 'G', d: 'Bear' }, - ]), - second: createSampleDatatableWithRows([ - { a: 10, b: 5, c: 'J', d: 'Bar' }, - { a: 8, b: 4, c: 'K', d: 'Fi' }, - { a: 1, b: 8, c: 'O', d: 'Pi' }, - ]), - }, - }; - const component = shallow( - <XYChart - data={{ - ...data, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), - }, - }} - args={multiLayerArgs} - formatFactory={getFormatSpy} - timeZone="UTC" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - histogramBarTarget={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} - /> - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); }); test('it does not use date range if the x is not a time scale', () => { @@ -698,7 +580,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -716,7 +598,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -737,7 +619,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -758,7 +640,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -784,7 +666,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -808,7 +690,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -893,7 +775,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -945,7 +827,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -983,7 +865,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1004,7 +886,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1028,7 +910,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1061,7 +943,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1081,7 +963,7 @@ describe('xy_expression', () => { timeZone="CEST" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1107,7 +989,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1127,7 +1009,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1150,7 +1032,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1178,7 +1060,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1200,7 +1082,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1601,7 +1483,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1621,7 +1503,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1641,7 +1523,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1660,7 +1542,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} timeZone="UTC" onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1683,7 +1565,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1718,7 +1600,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1751,7 +1633,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1784,7 +1666,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1817,7 +1699,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1917,7 +1799,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -1991,7 +1873,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2063,7 +1945,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2087,7 +1969,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2110,7 +1992,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2133,7 +2015,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2168,7 +2050,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2195,7 +2077,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2217,7 +2099,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2244,7 +2126,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2277,7 +2159,7 @@ describe('xy_expression', () => { timeZone="UTC" chartsThemeService={chartsThemeService} paletteService={paletteService} - histogramBarTarget={50} + minInterval={50} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -2288,4 +2170,47 @@ describe('xy_expression', () => { }); }); }); + + describe('calculateMinInterval', () => { + let xyProps: XYChartProps; + + beforeEach(() => { + xyProps = sampleArgs(); + xyProps.args.layers[0].xScaleType = 'time'; + }); + it('should use first valid layer and determine interval', async () => { + const result = await calculateMinInterval( + xyProps, + jest.fn().mockResolvedValue({ interval: '5m' }) + ); + expect(result).toEqual(5 * 60 * 1000); + }); + + it('should return undefined if data table is empty', async () => { + xyProps.data.tables.first.rows = []; + const result = await calculateMinInterval( + xyProps, + jest.fn().mockResolvedValue({ interval: '5m' }) + ); + expect(result).toEqual(undefined); + }); + + it('should return undefined if interval can not be checked', async () => { + const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + expect(result).toEqual(undefined); + }); + + it('should return undefined if date column is not found', async () => { + xyProps.data.tables.first.columns.splice(2, 1); + const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + expect(result).toEqual(undefined); + }); + + it('should return undefined if x axis is not a date', async () => { + xyProps.args.layers[0].xScaleType = 'ordinal'; + xyProps.data.tables.first.columns.splice(2, 1); + const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + expect(result).toEqual(undefined); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 8713e3989a1b6..54ae3bb759d2c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -8,7 +8,6 @@ import './expression.scss'; import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; -import moment from 'moment'; import { Chart, Settings, @@ -39,10 +38,14 @@ import { LensFilterEvent, LensBrushEvent, } from '../types'; -import { XYArgs, SeriesType, visualizationTypes } from './types'; +import { XYArgs, SeriesType, visualizationTypes, LayerArgs } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart, getSeriesColor } from './state_helpers'; -import { ExpressionValueSearchContext, search } from '../../../../../src/plugins/data/public'; +import { + DataPublicPluginStart, + ExpressionValueSearchContext, + search, +} from '../../../../../src/plugins/data/public'; import { ChartsPluginSetup, PaletteRegistry, @@ -75,7 +78,7 @@ type XYChartRenderProps = XYChartProps & { paletteService: PaletteRegistry; formatFactory: FormatFactory; timeZone: string; - histogramBarTarget: number; + minInterval: number | undefined; onClickValue: (data: LensFilterEvent['data']) => void; onSelectRange: (data: LensBrushEvent['data']) => void; }; @@ -174,11 +177,31 @@ export const xyChart: ExpressionFunctionDefinition< }, }; +export async function calculateMinInterval( + { args: { layers }, data }: XYChartProps, + getIntervalByColumn: DataPublicPluginStart['search']['aggs']['getDateMetaByDatatableColumn'] +) { + const filteredLayers = getFilteredLayers(layers, data); + if (filteredLayers.length === 0) return; + const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); + + if (!isTimeViz) return; + const dateColumn = data.tables[filteredLayers[0].layerId].columns.find( + (column) => column.id === filteredLayers[0].xAccessor + ); + if (!dateColumn) return; + const dateMetaData = await getIntervalByColumn(dateColumn); + if (!dateMetaData) return; + const intervalDuration = search.aggs.parseInterval(dateMetaData.interval); + if (!intervalDuration) return; + return intervalDuration.as('milliseconds'); +} + export const getXyChartRenderer = (dependencies: { formatFactory: Promise<FormatFactory>; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; - histogramBarTarget: number; + getIntervalByColumn: DataPublicPluginStart['search']['aggs']['getDateMetaByDatatableColumn']; timeZone: string; }): ExpressionRenderDefinition<XYChartProps> => ({ name: 'lens_xy_chart_renderer', @@ -209,7 +232,7 @@ export const getXyChartRenderer = (dependencies: { chartsThemeService={dependencies.chartsThemeService} paletteService={dependencies.paletteService} timeZone={dependencies.timeZone} - histogramBarTarget={dependencies.histogramBarTarget} + minInterval={await calculateMinInterval(config, dependencies.getIntervalByColumn)} onClickValue={onClickValue} onSelectRange={onSelectRange} /> @@ -277,7 +300,7 @@ export function XYChart({ timeZone, chartsThemeService, paletteService, - histogramBarTarget, + minInterval, onClickValue, onSelectRange, }: XYChartRenderProps) { @@ -285,19 +308,7 @@ export function XYChart({ const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); - const filteredLayers = layers.filter(({ layerId, xAccessor, accessors, splitAccessor }) => { - return !( - !accessors.length || - !data.tables[layerId] || - data.tables[layerId].rows.length === 0 || - (xAccessor && - data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) || - // stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty - (!xAccessor && - splitAccessor && - data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined')) - ); - }); + const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) { const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar'; @@ -348,37 +359,6 @@ export function XYChart({ filteredBarLayers.some((layer) => layer.accessors.length > 1) || filteredBarLayers.some((layer) => layer.splitAccessor); - function calculateMinInterval() { - // check all the tables to see if all of the rows have the same timestamp - // that would mean that chart will draw a single bar - const isSingleTimestampInXDomain = () => { - const firstRowValue = - data.tables[filteredLayers[0].layerId].rows[0][filteredLayers[0].xAccessor!]; - for (const layer of filteredLayers) { - if ( - layer.xAccessor && - data.tables[layer.layerId].rows.some((row) => row[layer.xAccessor!] !== firstRowValue) - ) { - return false; - } - } - return true; - }; - - // add minInterval only for single point in domain - if (data.dateRange && isSingleTimestampInXDomain()) { - const params = xAxisColumn?.meta?.sourceParams?.params as Record<string, string>; - if (params?.interval !== 'auto') - return search.aggs.parseInterval(params?.interval)?.asMilliseconds(); - - const { fromDate, toDate } = data.dateRange; - const duration = moment(toDate).diff(moment(fromDate)); - const targetMs = duration / histogramBarTarget; - return isNaN(targetMs) ? 0 : Math.max(Math.floor(targetMs), 1); - } - return undefined; - } - const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); const isHistogramViz = filteredLayers.every((l) => l.isHistogram); @@ -386,7 +366,7 @@ export function XYChart({ ? { min: data.dateRange?.fromDate.getTime(), max: data.dateRange?.toDate.getTime(), - minInterval: calculateMinInterval(), + minInterval, } : undefined; @@ -802,6 +782,22 @@ export function XYChart({ ); } +function getFilteredLayers(layers: LayerArgs[], data: LensMultiTable) { + return layers.filter(({ layerId, xAccessor, accessors, splitAccessor }) => { + return !( + !accessors.length || + !data.tables[layerId] || + data.tables[layerId].rows.length === 0 || + (xAccessor && + data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) || + // stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty + (!xAccessor && + splitAccessor && + data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined')) + ); + }); +} + function assertNever(x: never): never { throw new Error('Unexpected series type: ' + x); } diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 5e5eef2f01c17..ff719c222c5fa 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -7,7 +7,6 @@ import { CoreSetup, IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; -import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { EditorFrameSetup, FormatFactory } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { LensPluginStartDependencies } from '../plugin'; @@ -63,7 +62,7 @@ export class XyVisualization { chartsThemeService: charts.theme, paletteService: palettes, timeZone: getTimeZone(core.uiSettings), - histogramBarTarget: core.uiSettings.get<number>(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + getIntervalByColumn: data.search.aggs.getDateMetaByDatatableColumn, }) ); return getXyVisualization({ paletteService: palettes, data }); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts index 3150cb9975f21..ff39d91be7e4a 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts @@ -46,3 +46,13 @@ export const getCreateExceptionListMinimalSchemaMockWithoutId = (): CreateExcept name: NAME, type: ENDPOINT_TYPE, }); + +/** + * Useful for end to end testing with detections + */ +export const getCreateExceptionListDetectionSchemaMock = (): CreateExceptionListSchema => ({ + description: DESCRIPTION, + list_id: LIST_ID, + name: NAME, + type: 'detection', +}); diff --git a/x-pack/plugins/maps/public/components/action_select.tsx b/x-pack/plugins/maps/public/components/action_select.tsx index ad61a6a129974..8ea9334bba753 100644 --- a/x-pack/plugins/maps/public/components/action_select.tsx +++ b/x-pack/plugins/maps/public/components/action_select.tsx @@ -8,6 +8,7 @@ import React, { Component } from 'react'; import { EuiFormRow, EuiSuperSelect, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; +import { isUrlDrilldown } from '../trigger_actions/trigger_utils'; interface Props { value?: string; @@ -41,7 +42,7 @@ export class ActionSelect extends Component<Props, State> { } const actions = await this.props.getFilterActions(); if (this._isMounted) { - this.setState({ actions }); + this.setState({ actions: actions.filter((action) => !isUrlDrilldown(action)) }); } } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/_map_container.scss b/x-pack/plugins/maps/public/connected_components/map_container/_map_container.scss index 2180573ef4583..7ec7d0d47ed04 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/_map_container.scss +++ b/x-pack/plugins/maps/public/connected_components/map_container/_map_container.scss @@ -1,5 +1,4 @@ .mapMapWrapper { - background-color: $euiColorEmptyShade; position: relative; } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/index.ts b/x-pack/plugins/maps/public/connected_components/map_container/index.ts index c4b5cc51fb210..37ee3a630066d 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_container/index.ts @@ -14,6 +14,7 @@ import { areLayersLoaded, getRefreshConfig, getMapInitError, + getMapSettings, getQueryableUniqueIndexPatternIds, isToolbarOverlayHidden, } from '../../selectors/map_selectors'; @@ -29,6 +30,7 @@ function mapStateToProps(state: MapStoreState) { mapInitError: getMapInitError(state), indexPatternIds: getQueryableUniqueIndexPatternIds(state), hideToolbarOverlay: isToolbarOverlayHidden(state), + backgroundColor: getMapSettings(state).backgroundColor, }; } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 169875e63a536..9a5110a0c24d2 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -23,7 +23,7 @@ import { LayerPanel } from '../layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; import { getIndexPatternsFromIds } from '../../index_pattern_util'; -import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; +import { ES_GEO_FIELD_TYPE, RawValue } from '../../../common/constants'; import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettingsPanel } from '../map_settings_panel'; @@ -37,8 +37,10 @@ const RENDER_COMPLETE_EVENT = 'renderComplete'; interface Props { addFilters: ((filters: Filter[]) => Promise<void>) | null; + backgroundColor: string; getFilterActions?: () => Promise<Action[]>; getActionContext?: () => ActionExecutionContext; + onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; areLayersLoaded: boolean; cancelAllInFlightRequests: () => void; exitFullScreen: () => void; @@ -190,6 +192,7 @@ export class MapContainer extends Component<Props, State> { addFilters, getFilterActions, getActionContext, + onSingleValueTrigger, flyoutDisplay, isFullScreen, exitFullScreen, @@ -241,11 +244,15 @@ export class MapContainer extends Component<Props, State> { data-title={this.props.title} data-description={this.props.description} > - <EuiFlexItem className="mapMapWrapper"> + <EuiFlexItem + className="mapMapWrapper" + style={{ backgroundColor: this.props.backgroundColor }} + > <MBMap addFilters={addFilters} getFilterActions={getFilterActions} getActionContext={getActionContext} + onSingleValueTrigger={onSingleValueTrigger} geoFields={this.state.geoFields} renderTooltipContent={renderTooltipContent} /> diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_chrome_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_chrome_panel.tsx new file mode 100644 index 0000000000000..09e3d270fcf2c --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_chrome_panel.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFormRow, EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { MbValidatedColorPicker } from '../../classes/styles/vector/components/color/mb_validated_color_picker'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function MapChromePanel({ settings, updateMapSetting }: Props) { + const onBackgroundColorChange = (color: string) => { + updateMapSetting('backgroundColor', color); + }; + + return ( + <EuiPanel> + <EuiTitle size="xs"> + <h5> + <FormattedMessage id="xpack.maps.mapSettingsPanel.mapTitle" defaultMessage="Map" /> + </h5> + </EuiTitle> + + <EuiFormRow + label={i18n.translate('xpack.maps.mapSettingsPanel.backgroundColorLabel', { + defaultMessage: 'Background color', + })} + display="columnCompressed" + > + <MbValidatedColorPicker + color={settings.backgroundColor} + onChange={onBackgroundColorChange} + /> + </EuiFormRow> + </EuiPanel> + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx index 5bc06031f3516..02461a6c0ba5c 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -20,6 +20,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { MapSettings } from '../../reducers/map'; import { NavigationPanel } from './navigation_panel'; import { SpatialFiltersPanel } from './spatial_filters_panel'; +import { MapChromePanel } from './map_chrome_panel'; import { MapCenter } from '../../../common/descriptor_types'; interface Props { @@ -65,6 +66,8 @@ export function MapSettingsPanel({ <div className="mapLayerPanel__body"> <div className="mapLayerPanel__bodyOverflow"> + <MapChromePanel settings={settings} updateMapSetting={updateMapSetting} /> + <EuiSpacer size="s" /> <NavigationPanel center={center} settings={settings} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js index edd501f266690..97b47358ec089 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js @@ -15,6 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public'; +import { isUrlDrilldown } from '../../../trigger_actions/trigger_utils'; export class FeatureProperties extends React.Component { state = { @@ -114,21 +115,37 @@ export class FeatureProperties extends React.Component { _renderFilterActions(tooltipProperty) { const panel = { id: 0, - items: this.state.actions.map((action) => { - const actionContext = this.props.getActionContext(); - const iconType = action.getIconType(actionContext); - const name = action.getDisplayName(actionContext); - return { - name, - icon: iconType ? <EuiIcon type={iconType} /> : null, - onClick: async () => { - this.props.onCloseTooltip(); - const filters = await tooltipProperty.getESFilters(); - this.props.addFilters(filters, action.id); - }, - ['data-test-subj']: `mapFilterActionButton__${name}`, - }; - }), + items: this.state.actions + .filter((action) => { + if (isUrlDrilldown(action)) { + return !!this.props.onSingleValueTrigger; + } + return true; + }) + .map((action) => { + const actionContext = this.props.getActionContext(); + const iconType = action.getIconType(actionContext); + const name = action.getDisplayName(actionContext); + return { + name: name ? name : action.id, + icon: iconType ? <EuiIcon type={iconType} /> : null, + onClick: async () => { + this.props.onCloseTooltip(); + + if (isUrlDrilldown(action)) { + this.props.onSingleValueTrigger( + action.id, + tooltipProperty.getPropertyKey(), + tooltipProperty.getRawValue() + ); + } else { + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters, action.id); + } + }, + ['data-test-subj']: `mapFilterActionButton__${name}`, + }; + }), }; return ( diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js index 8547219b42e30..60d9e57d15e23 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js @@ -183,6 +183,7 @@ export class FeaturesTooltip extends Component { addFilters={this.props.addFilters} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} + onSingleValueTrigger={this.props.onSingleValueTrigger} showFilterActions={this._showFilterActionsView} /> {this._renderActions(geoFields)} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js index 04c376a093623..0ea40f6e3182f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js @@ -323,6 +323,7 @@ export class MBMap extends React.Component { addFilters={this.props.addFilters} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} + onSingleValueTrigger={this.props.onSingleValueTrigger} geoFields={this.props.geoFields} renderTooltipContent={this.props.renderTooltipContent} /> diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js index b178eef6fa5d3..c5c3ad4d78f7e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js @@ -201,6 +201,7 @@ export class TooltipControl extends React.Component { addFilters={this.props.addFilters} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} + onSingleValueTrigger={this.props.onSingleValueTrigger} renderTooltipContent={this.props.renderTooltipContent} geoFields={this.props.geoFields} features={features} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js index ca4864f79940e..4983e394ed93c 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js @@ -119,6 +119,7 @@ export class TooltipPopover extends Component { addFilters: this.props.addFilters, getFilterActions: this.props.getFilterActions, getActionContext: this.props.getActionContext, + onSingleValueTrigger: this.props.onSingleValueTrigger, closeTooltip: this.props.closeTooltip, features: this.props.features, isLocked: this.props.isLocked, diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index caf21431145d5..7aaabc427790a 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -18,6 +18,7 @@ import { import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; import { APPLY_FILTER_TRIGGER, + VALUE_CLICK_TRIGGER, ActionExecutionContext, TriggerContextMapping, } from '../../../../../src/plugins/ui_actions/public'; @@ -57,6 +58,7 @@ import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE, MAP_PATH, + RawValue, } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; import { getUiActions, getCoreI18n, getHttp } from '../kibana_services'; @@ -65,6 +67,7 @@ import { MapContainer } from '../connected_components/map_container'; import { SavedMap } from '../routes/map_page'; import { getIndexPatternsFromIds } from '../index_pattern_util'; import { getMapAttributeService } from '../map_attribute_service'; +import { isUrlDrilldown, toValueClickDataFormat } from '../trigger_actions/trigger_utils'; import { MapByValueInput, @@ -202,7 +205,7 @@ export class MapEmbeddable } public supportedTriggers(): Array<keyof TriggerContextMapping> { - return [APPLY_FILTER_TRIGGER]; + return [APPLY_FILTER_TRIGGER, VALUE_CLICK_TRIGGER]; } setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => { @@ -290,6 +293,7 @@ export class MapEmbeddable <Provider store={this._savedMap.getStore()}> <I18nContext> <MapContainer + onSingleValueTrigger={this.onSingleValueTrigger} addFilters={this.input.hideFilterActions ? null : this.addFilters} getFilterActions={this.getFilterActions} getActionContext={this.getActionContext} @@ -320,6 +324,20 @@ export class MapEmbeddable return await getIndexPatternsFromIds(queryableIndexPatternIds); } + onSingleValueTrigger = (actionId: string, key: string, value: RawValue) => { + const action = getUiActions().getAction(actionId); + if (!action) { + throw new Error('Unable to apply action, could not locate action'); + } + const executeContext = { + ...this.getActionContext(), + data: { + data: toValueClickDataFormat(key, value), + }, + }; + action.execute(executeContext); + }; + addFilters = async (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => { const executeContext = { ...this.getActionContext(), @@ -333,10 +351,24 @@ export class MapEmbeddable }; getFilterActions = async () => { - return await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, { + const filterActions = await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, { embeddable: this, filters: [], }); + const valueClickActions = await getUiActions().getTriggerCompatibleActions( + VALUE_CLICK_TRIGGER, + { + embeddable: this, + data: { + // uiActions.getTriggerCompatibleActions validates action with provided context + // so if event.key and event.value are used in the URL template but can not be parsed from context + // then the action is filtered out. + // To prevent filtering out actions, provide dummy context when initially fetching actions. + data: toValueClickDataFormat('anyfield', 'anyvalue'), + }, + } + ); + return [...filterActions, ...valueClickActions.filter(isUrlDrilldown)]; }; getActionContext = () => { diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts index 896ac11e36782..e98af6f426b5a 100644 --- a/x-pack/plugins/maps/public/reducers/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { INITIAL_LOCATION, MAX_ZOOM, MIN_ZOOM } from '../../common/constants'; import { MapSettings } from './map'; export function getDefaultMapSettings(): MapSettings { return { autoFitToDataBounds: false, + backgroundColor: euiThemeVars.euiColorEmptyShade, initialLocation: INITIAL_LOCATION.LAST_SAVED_LOCATION, fixedLocation: { lat: 0, lon: 0, zoom: 2 }, browserLocation: { zoom: 2 }, diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index aca75334032d9..d4ac20c7114dc 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -43,6 +43,7 @@ export type MapContext = { export type MapSettings = { autoFitToDataBounds: boolean; + backgroundColor: string; initialLocation: INITIAL_LOCATION; fixedLocation: { lat: number; diff --git a/x-pack/plugins/maps/public/trigger_actions/trigger_utils.ts b/x-pack/plugins/maps/public/trigger_actions/trigger_utils.ts new file mode 100644 index 0000000000000..3505588a9c049 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/trigger_utils.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'src/plugins/ui_actions/public'; +import { RawValue } from '../../common/constants'; +import { DatatableColumnType } from '../../../../../src/plugins/expressions'; + +export function isUrlDrilldown(action: Action) { + // @ts-expect-error + return action.type === 'URL_DRILLDOWN'; +} + +// VALUE_CLICK_TRIGGER is coupled with expressions and Datatable type +// URL drilldown parses event scope from Datatable +// https://github.com/elastic/kibana/blob/7.10/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts#L140 +// In order to use URL drilldown, maps has to package its data in Datatable compatiable format. +export function toValueClickDataFormat(key: string, value: RawValue) { + return [ + { + table: { + columns: [ + { + id: key, + meta: { + type: 'unknown' as DatatableColumnType, // type is not used by URL drilldown to parse event but is required by DatatableColumnMeta + field: key, + }, + name: key, + }, + ], + rows: [ + { + [key]: value, + }, + ], + }, + column: 0, + row: 0, + value, + }, + ]; +} diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 958d5ae250185..7eef86869b9e5 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -16,7 +16,7 @@ export const JOB_MAP_NODE_TYPES = { ANALYTICS: 'analytics', TRANSFORM: 'transform', INDEX: 'index', - INFERENCE_MODEL: 'inferenceModel', + TRAINED_MODEL: 'trainedModel', } as const; export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 9a3d8fc4a4f02..b5a78ee746efe 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -156,6 +156,7 @@ export type TimeSeriesExplorerUrlState = MLPageState< export interface DataFrameAnalyticsQueryState { jobId?: JobId | JobId[]; + modelId?: string; groupIds?: string[]; globalState?: MlCommonGlobalState; } @@ -170,6 +171,7 @@ export interface DataFrameAnalyticsExplorationQueryState { jobId: JobId; analysisType: DataFrameAnalysisConfigType; defaultIsTraining?: boolean; + modelId?: string; }; } @@ -180,6 +182,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState< analysisType: DataFrameAnalysisConfigType; globalState?: MlCommonGlobalState; defaultIsTraining?: boolean; + modelId?: string; } >; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index a5d3555fcc278..bf90ce58fb85d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -15,10 +15,11 @@ interface Tab { path: string; } -export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }> = ({ - jobId, - selectedTabId, -}) => { +export const AnalyticsNavigationBar: FC<{ + selectedTabId?: string; + jobId?: string; + modelId?: string; +}> = ({ jobId, modelId, selectedTabId }) => { const navigateToPath = useNavigateToPath(); const tabs = useMemo(() => { @@ -38,7 +39,7 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string path: '/data_frame_analytics/models', }, ]; - if (jobId !== undefined) { + if (jobId !== undefined || modelId !== undefined) { navTabs.push({ id: 'map', name: i18n.translate('xpack.ml.dataframe.mapTabLabel', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 2d74d08c4550c..cde29d357b1c6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -342,7 +342,7 @@ export const ModelsList: FC = () => { onClick: async (item) => { const path = await mlUrlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, - pageState: { jobId: item.metadata?.analytics_config.id }, + pageState: { modelId: item.model_id }, }); await navigateToPath(path, false); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 5a17b91818a1c..38b7088690e12 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -59,6 +59,7 @@ export const Page: FC = () => { const location = useLocation(); const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]); const mapJobId = globalState?.ml?.jobId; + const mapModelId = globalState?.ml?.modelId; return ( <Fragment> @@ -106,8 +107,14 @@ export const Page: FC = () => { <UpgradeWarning /> <EuiPageContent> - <AnalyticsNavigationBar selectedTabId={selectedTabId} jobId={mapJobId} /> - {selectedTabId === 'map' && mapJobId && <JobMap analyticsId={mapJobId} />} + <AnalyticsNavigationBar + selectedTabId={selectedTabId} + jobId={mapJobId} + modelId={mapModelId} + /> + {selectedTabId === 'map' && (mapJobId || mapModelId) && ( + <JobMap analyticsId={mapJobId} modelId={mapModelId} /> + )} {selectedTabId === 'data_frame_analytics' && ( <DataFrameAnalyticsList blockRefresh={blockRefresh} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss index d54b5214f7448..7fcd082a37230 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss @@ -5,7 +5,7 @@ .mlJobMapLegend__indexPattern { height: $euiSizeM; width: $euiSizeM; - background-color: '#FFFFFF'; + background-color: $euiColorGhost; border: 1px solid $euiColorVis2; transform: rotate(45deg); display: 'inline-block'; @@ -14,7 +14,7 @@ .mlJobMapLegend__transform { height: $euiSizeM; width: $euiSizeM; - background-color: '#FFFFFF'; + background-color: $euiColorGhost; border: 1px solid $euiColorVis1; display: 'inline-block'; } @@ -22,17 +22,26 @@ .mlJobMapLegend__analytics { height: $euiSizeM; width: $euiSizeM; - background-color: '#FFFFFF'; + background-color: $euiColorGhost; border: 1px solid $euiColorVis0; - border-radius: 50%; + border-radius: $euiBorderRadius; display: 'inline-block'; } -.mlJobMapLegend__inferenceModel { +.mlJobMapLegend__trainedModel { height: $euiSizeM; width: $euiSizeM; - background-color: '#FFFFFF'; - border: 1px solid $euiColorMediumShade; - border-radius: 50%; + background-color: $euiColorGhost; + border: $euiBorderThin; + border-radius: $euiBorderRadius; + display: 'inline-block'; +} + +.mlJobMapLegend__sourceNode { + height: $euiSizeM; + width: $euiSizeM; + background-color: $euiColorLightShade; + border: $euiBorderThin; + border-radius: $euiBorderRadius; display: 'inline-block'; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index ed25ea6cbf02c..f5738c20b2c3f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -25,10 +25,11 @@ import { EuiDescriptionListProps } from '@elastic/eui/src/components/description import { CytoscapeContext } from './cytoscape'; import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/util/date_utils'; import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; -// import { DeleteButton } from './delete_button'; +// import { DeleteButton } from './delete_button'; // TODO: add delete functionality in followup interface Props { - analyticsId: string; + analyticsId?: string; + modelId?: string; details: any; getNodeData: any; } @@ -56,7 +57,7 @@ function getListItems(details: object): EuiDescriptionListProps['listItems'] { }); } -export const Controls: FC<Props> = ({ analyticsId, details, getNodeData }) => { +export const Controls: FC<Props> = ({ analyticsId, modelId, details, getNodeData }) => { const [showFlyout, setShowFlyout] = useState<boolean>(false); const [selectedNode, setSelectedNode] = useState<cytoscape.NodeSingular | undefined>(); @@ -98,10 +99,12 @@ export const Controls: FC<Props> = ({ analyticsId, details, getNodeData }) => { } const nodeDataButton = - analyticsId !== nodeLabel && nodeType === JOB_MAP_NODE_TYPES.ANALYTICS ? ( + analyticsId !== nodeLabel && + modelId !== nodeLabel && + (nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX) ? ( <EuiButtonEmpty onClick={() => { - getNodeData(nodeLabel); + getNodeData({ id: nodeLabel, type: nodeType }); setShowFlyout(false); }} iconType="branch" diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index 85d10aa897415..18be614afb5c3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -80,7 +80,8 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { { selector: 'node', style: { - 'background-color': theme.euiColorGhost, + 'background-color': (el: cytoscape.NodeSingular) => + el.data('isRoot') ? theme.euiColorLightShade : theme.euiColorGhost, 'background-height': '60%', 'background-width': '60%', 'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el), diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx index c29b6aca804d7..04e415eca1691 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -6,6 +6,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; export const JobMapLegend: FC = () => ( @@ -17,7 +18,10 @@ export const JobMapLegend: FC = () => ( </EuiFlexItem> <EuiFlexItem grow={false}> <EuiText size="xs" color="subdued"> - {JOB_MAP_NODE_TYPES.INDEX} + <FormattedMessage + id="xpack.ml.dataframe.analyticsMap.legend.indexLabel" + defaultMessage="index" + /> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -41,7 +45,10 @@ export const JobMapLegend: FC = () => ( </EuiFlexItem> <EuiFlexItem grow={false}> <EuiText size="xs" color="subdued"> - {JOB_MAP_NODE_TYPES.ANALYTICS} + <FormattedMessage + id="xpack.ml.dataframe.analyticsMap.legend.analyticsJobLabel" + defaultMessage="analytics job" + /> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -49,11 +56,29 @@ export const JobMapLegend: FC = () => ( <EuiFlexItem grow={false}> <EuiFlexGroup gutterSize="xs" alignItems="center"> <EuiFlexItem grow={false}> - <span className="mlJobMapLegend__inferenceModel" /> + <span className="mlJobMapLegend__trainedModel" /> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiText size="xs" color="subdued"> - {'inference model'} + <FormattedMessage + id="xpack.ml.dataframe.analyticsMap.legend.trainedModelLabel" + defaultMessage="trained model" + /> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="xs" alignItems="center"> + <EuiFlexItem grow={false}> + <span className="mlJobMapLegend__sourceNode" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="xs" color="subdued"> + <FormattedMessage + id="xpack.ml.dataframe.analyticsMap.legend.rootNodeLabel" + defaultMessage="source node" + /> </EuiText> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx index 53d47937409d8..6395d491d5e6b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -15,6 +15,7 @@ import { Cytoscape, Controls, JobMapLegend } from './components'; import { ml } from '../../../services/ml_api_service'; import { useMlKibana } from '../../../contexts/kibana'; import { useRefDimensions } from './components/use_ref_dimensions'; +import { JOB_MAP_NODE_TYPES } from '../../../../../common/constants/data_frame_analytics'; const cytoscapeDivStyle = { background: `linear-gradient( @@ -36,22 +37,36 @@ ${theme.euiColorLightShade}`, marginTop: 0, }; -export const JobMapTitle: React.FC<{ analyticsId: string }> = ({ analyticsId }) => ( +export const JobMapTitle: React.FC<{ analyticsId?: string; modelId?: string }> = ({ + analyticsId, + modelId, +}) => ( <EuiTitle size="xs"> <span> - {i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', { - defaultMessage: 'Map for analytics ID {analyticsId}', - values: { analyticsId }, - })} + {analyticsId + ? i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', { + defaultMessage: 'Map for analytics ID {analyticsId}', + values: { analyticsId }, + }) + : i18n.translate('xpack.ml.dataframe.analyticsMap.modelIdTitle', { + defaultMessage: 'Map for trained model ID {modelId}', + values: { modelId }, + })} </span> </EuiTitle> ); +interface GetDataObjectParameter { + id: string; + type: string; +} + interface Props { - analyticsId: string; + analyticsId?: string; + modelId?: string; } -export const JobMap: FC<Props> = ({ analyticsId }) => { +export const JobMap: FC<Props> = ({ analyticsId, modelId }) => { const [elements, setElements] = useState<cytoscape.ElementDefinition[]>([]); const [nodeDetails, setNodeDetails] = useState({}); const [error, setError] = useState(undefined); @@ -60,14 +75,33 @@ export const JobMap: FC<Props> = ({ analyticsId }) => { services: { notifications }, } = useMlKibana(); - const getData = async (id?: string) => { + const getDataWrapper = async (params?: GetDataObjectParameter) => { + const { id, type } = params ?? {}; const treatAsRoot = id !== undefined; - const idToUse = treatAsRoot ? id : analyticsId; - // Pass in treatAsRoot flag - endpoint will take job destIndex to grab jobs created from it + let idToUse: string; + + if (id !== undefined) { + idToUse = id; + } else if (modelId !== undefined) { + idToUse = modelId; + } else { + idToUse = analyticsId as string; + } + + await getData( + idToUse, + treatAsRoot, + modelId !== undefined && treatAsRoot === false ? JOB_MAP_NODE_TYPES.TRAINED_MODEL : type + ); + }; + + const getData = async (idToUse: string, treatAsRoot: boolean, type?: string) => { + // Pass in treatAsRoot flag - endpoint will take job or index to grab jobs created from it // TODO: update analyticsMap return type here const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap( idToUse, - treatAsRoot + treatAsRoot, + type ); const { elements: nodeElements, details, error: fetchError } = analyticsMap; @@ -86,7 +120,7 @@ export const JobMap: FC<Props> = ({ analyticsId }) => { } if (nodeElements && nodeElements.length > 0) { - if (id === undefined) { + if (treatAsRoot === false) { setElements(nodeElements); setNodeDetails(details); } else { @@ -98,8 +132,8 @@ export const JobMap: FC<Props> = ({ analyticsId }) => { }; useEffect(() => { - getData(); - }, [analyticsId]); + getDataWrapper(); + }, [analyticsId, modelId]); if (error !== undefined) { notifications.toasts.addDanger( @@ -119,14 +153,19 @@ export const JobMap: FC<Props> = ({ analyticsId }) => { <div style={{ height: height - parseInt(theme.gutterTypes.gutterLarge, 10) }} ref={ref}> <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem grow={false}> - <JobMapTitle analyticsId={analyticsId} /> + <JobMapTitle analyticsId={analyticsId} modelId={modelId} /> </EuiFlexItem> <EuiFlexItem grow={false}> <JobMapLegend /> </EuiFlexItem> </EuiFlexGroup> <Cytoscape height={height} elements={elements} width={width} style={cytoscapeDivStyle}> - <Controls details={nodeDetails} getNodeData={getData} analyticsId={analyticsId} /> + <Controls + details={nodeDetails} + getNodeData={getDataWrapper} + analyticsId={analyticsId} + modelId={modelId} + /> </Cytoscape> </div> </> diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 21556a4702b4e..8e541443c34a1 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -83,12 +83,12 @@ export const dataFrameAnalytics = { body, }); }, - getDataFrameAnalyticsMap(analyticsId?: string, treatAsRoot?: boolean) { - const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; + getDataFrameAnalyticsMap(id: string, treatAsRoot: boolean, type?: string) { + const idString = id !== undefined ? `/${id}` : ''; return http({ - path: `${basePath()}/data_frame/analytics/map${analyticsIdString}`, + path: `${basePath()}/data_frame/analytics/map${idString}`, method: 'GET', - query: { treatAsRoot }, + query: { treatAsRoot, type }, }); }, evaluateDataFrameAnalytics(evaluateConfig: any) { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx index 8e26a912a6051..78c0cb97cb889 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx @@ -38,12 +38,14 @@ export const PlotByFunctionControls = ({ selectedDetectorIndex, selectedJobId, selectedEntities, + entityControlsCount, }: { functionDescription: undefined | string; setFunctionDescription: (func: string) => void; selectedDetectorIndex: number; selectedJobId: string; selectedEntities: Record<string, any>; + entityControlsCount: number; }) => { const toastNotificationService = useToastNotificationService(); @@ -73,9 +75,12 @@ export const PlotByFunctionControls = ({ return; } const selectedJob = mlJobService.getJob(selectedJobId); + // if no controls, it's okay to fetch + // if there are series controls, only fetch if user has selected something + const validEntities = + entityControlsCount === 0 || (entityControlsCount > 0 && selectedEntities !== undefined); if ( - // set if only entity controls are picked - selectedEntities !== undefined && + validEntities && functionDescription === undefined && isMetricDetector(selectedJob, selectedDetectorIndex) ) { @@ -95,6 +100,7 @@ export const PlotByFunctionControls = ({ selectedEntities, selectedJobId, functionDescription, + entityControlsCount, ]); if (functionDescription === undefined) return null; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx index 37a637e2c1446..c1f35e68e43c6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx @@ -28,6 +28,7 @@ import { useStorage } from '../../../contexts/ml/use_storage'; import { EntityFieldType } from '../../../../../common/types/anomalies'; import { FieldDefinition } from '../../../services/results_service/result_service_rx'; import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors'; +import { PlotByFunctionControls } from '../plot_function_controls'; function getEntityControlOptions(fieldValues: FieldDefinition['values']): ComboBoxOption[] { if (!Array.isArray(fieldValues)) { @@ -67,6 +68,8 @@ interface SeriesControlsProps { bounds: any; appStateHandler: Function; selectedEntities: Record<string, any>; + functionDescription: string; + setFunctionDescription: (func: string) => void; } /** @@ -79,6 +82,8 @@ export const SeriesControls: FC<SeriesControlsProps> = ({ appStateHandler, children, selectedEntities, + functionDescription, + setFunctionDescription, }) => { const { services: { @@ -306,6 +311,15 @@ export const SeriesControls: FC<SeriesControlsProps> = ({ /> ); })} + <PlotByFunctionControls + selectedJobId={selectedJobId} + selectedDetectorIndex={selectedDetectorIndex} + selectedEntities={selectedEntities} + functionDescription={functionDescription} + setFunctionDescription={setFunctionDescription} + entityControlsCount={entityControls.length} + /> + {children} </EuiFlexGroup> </div> diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index f22cc191ef844..47d0f25857b03 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -81,7 +81,6 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/ import { getControlsForDetector } from './get_controls_for_detector'; import { SeriesControls } from './components/series_controls'; import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip'; -import { PlotByFunctionControls } from './components/plot_function_controls'; import { aggregationTypeTransform } from '../../../common/util/anomaly_utils'; import { isMetricDetector } from './get_function_description'; import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors'; @@ -1013,15 +1012,9 @@ export class TimeSeriesExplorer extends React.Component { selectedDetectorIndex={selectedDetectorIndex} selectedEntities={this.props.selectedEntities} bounds={bounds} + functionDescription={this.props.functionDescription} + setFunctionDescription={this.setFunctionDescription} > - <PlotByFunctionControls - selectedJobId={selectedJobId} - selectedDetectorIndex={selectedDetectorIndex} - selectedEntities={this.props.selectedEntities} - functionDescription={this.props.functionDescription} - setFunctionDescription={this.setFunctionDescription} - /> - {arePartitioningFieldsProvided && ( <EuiFlexItem style={{ textAlign: 'right' }}> <EuiFormRow hasEmptyLabelSpace style={{ maxWidth: '100%' }}> diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index dc9c3bd86cc63..10764022a3ce7 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -104,11 +104,12 @@ export function createDataFrameAnalyticsMapUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`; if (mlUrlGeneratorState) { - const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; + const { jobId, modelId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; const queryState: DataFrameAnalyticsExplorationQueryState = { ml: { jobId, + modelId, analysisType, defaultIsTraining, }, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index f1f0b352ca920..769ec09a6b911 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -10,12 +10,17 @@ import { JOB_MAP_NODE_TYPES, JobMapNodeTypes, } from '../../../common/constants/data_frame_analytics'; +import { TrainedModelConfigResponse } from '../../../common/types/trained_models'; import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { AnalyticsMapEdgeElement, AnalyticsMapReturnType, AnalyticsMapNodeElement, + ExtendAnalyticsMapArgs, + GetAnalyticsMapArgs, + InitialElementsReturnType, + isCompleteInitialReturnType, isAnalyticsMapEdgeElement, isAnalyticsMapNodeElement, isIndexPatternLinkReturnType, @@ -29,7 +34,7 @@ import type { MlClient } from '../../lib/ml_client'; export class AnalyticsManager { private _client: IScopedClusterClient['asInternalUser']; private _mlClient: MlClient; - public _inferenceModels: any; // TODO: update types + public _inferenceModels: TrainedModelConfigResponse[]; constructor(mlClient: MlClient, client: IScopedClusterClient['asInternalUser']) { this._client = client; @@ -37,11 +42,11 @@ export class AnalyticsManager { this._inferenceModels = []; } - public set inferenceModels(models: any) { + public set inferenceModels(models) { this._inferenceModels = models; } - public get inferenceModels(): any { + public get inferenceModels() { return this._inferenceModels; } @@ -56,16 +61,20 @@ export class AnalyticsManager { } } - private isDuplicateElement(analyticsId: string, elements: any[]): boolean { + private isDuplicateElement(analyticsId: string, elements: MapElements[]): boolean { let isDuplicate = false; - elements.forEach((elem: any) => { - if (elem.data.label === analyticsId && elem.data.type === JOB_MAP_NODE_TYPES.ANALYTICS) { + elements.forEach((elem) => { + if ( + isAnalyticsMapNodeElement(elem) && + elem.data.label === analyticsId && + elem.data.type === JOB_MAP_NODE_TYPES.ANALYTICS + ) { isDuplicate = true; } }); return isDuplicate; } - // @ts-ignore // TODO: is this needed? + private async getAnalyticsModelData(modelId: string) { const resp = await this._mlClient.getTrainedModels({ model_id: modelId, @@ -80,11 +89,17 @@ export class AnalyticsManager { return models; } - private async getAnalyticsJobData(analyticsId: string) { - const resp = await this._mlClient.getDataFrameAnalytics({ - id: analyticsId, - }); - const jobData = resp?.body?.data_frame_analytics[0]; + private async getAnalyticsData(analyticsId?: string) { + const options = analyticsId + ? { + id: analyticsId, + } + : undefined; + const resp = await this._mlClient.getDataFrameAnalytics(options); + const jobData = analyticsId + ? resp?.body?.data_frame_analytics[0] + : resp?.body?.data_frame_analytics; + return jobData; } @@ -130,7 +145,7 @@ export class AnalyticsManager { return { isWildcardIndexPattern, isIndexPattern: true, indexData, meta }; } else if (type.includes(JOB_MAP_NODE_TYPES.ANALYTICS)) { // fetch job associated with this index - const jobData = await this.getAnalyticsJobData(id); + const jobData = await this.getAnalyticsData(id); return { jobData, isJob: true }; } else if (type === JOB_MAP_NODE_TYPES.TRANSFORM) { // fetch transform so we can get original index pattern @@ -155,12 +170,12 @@ export class AnalyticsManager { let edgeElement; if (analyticsModel !== undefined) { - const modelId = `${analyticsModel.model_id}-${JOB_MAP_NODE_TYPES.INFERENCE_MODEL}`; + const modelId = `${analyticsModel.model_id}-${JOB_MAP_NODE_TYPES.TRAINED_MODEL}`; modelElement = { data: { id: modelId, label: analyticsModel.model_id, - type: JOB_MAP_NODE_TYPES.INFERENCE_MODEL, + type: JOB_MAP_NODE_TYPES.TRAINED_MODEL, }, }; // Create edge for job and corresponding model @@ -201,29 +216,41 @@ export class AnalyticsManager { } /** - * Works backward from jobId to return related jobs from source indices - * @param jobId + * Prepares the initial elements for incoming modelId + * @param modelId */ - async getAnalyticsMap(analyticsId: string): Promise<AnalyticsMapReturnType> { - const result: any = { elements: [], details: {}, error: null }; - const modelElements: MapElements[] = []; - const indexPatternElements: MapElements[] = []; + async getInitialElementsModelRoot(modelId: string): Promise<InitialElementsReturnType> { + const resultElements = []; + const modelElements = []; + const details: any = {}; + // fetch model data and create model elements + let data = await this.getAnalyticsModelData(modelId); + const modelNodeId = `${data.model_id}-${JOB_MAP_NODE_TYPES.TRAINED_MODEL}`; + const sourceJobId = data?.metadata?.analytics_config?.id; + let nextLinkId: string | undefined; + let nextType: JobMapNodeTypes | undefined; + let previousNodeId: string | undefined; + + modelElements.push({ + data: { + id: modelNodeId, + label: data.model_id, + type: JOB_MAP_NODE_TYPES.TRAINED_MODEL, + isRoot: true, + }, + }); - try { - await this.setInferenceModels(); - // Create first node for incoming analyticsId - let data = await this.getAnalyticsJobData(analyticsId); - let nextLinkId = data?.source?.index[0]; - let nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX; - let complete = false; - let link: NextLinkReturnType; - let count = 0; - let rootTransform; - let rootIndexPattern; - - let previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + details[modelNodeId] = data; + // fetch source job data and create elements + if (sourceJobId !== undefined) { + data = await this.getAnalyticsData(sourceJobId); - result.elements.push({ + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + + previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + resultElements.push({ data: { id: previousNodeId, label: data.id, @@ -231,167 +258,178 @@ export class AnalyticsManager { analysisType: getAnalysisType(data?.analysis), }, }); - result.details[previousNodeId] = data; + // Create edge between job and model + modelElements.push({ + data: { + id: `${previousNodeId}~${modelNodeId}`, + source: previousNodeId, + target: modelNodeId, + }, + }); - let { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(analyticsId); - if (isAnalyticsMapNodeElement(modelElement)) { - modelElements.push(modelElement); - result.details[modelElement.data.id] = modelDetails; - } - if (isAnalyticsMapEdgeElement(edgeElement)) { - modelElements.push(edgeElement); - } - // Add a safeguard against infinite loops. - while (complete === false) { - count++; - if (count >= 100) { - break; - } + details[previousNodeId] = data; + } - try { - link = await this.getNextLink({ - id: nextLinkId, - type: nextType, - }); - } catch (error) { - result.error = error.message || 'Something went wrong'; - break; - } - // If it's index pattern, check meta data to see what to fetch next - if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { - if (link.isWildcardIndexPattern === true) { - // Create index nodes for each of the indices included in the index pattern then break - const { details, elements } = this.getIndexPatternElements( - link.indexData, - previousNodeId - ); - - indexPatternElements.push(...elements); - result.details = { ...result.details, ...details }; - complete = true; - } else { - const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; - result.elements.unshift({ - data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, - }); - result.details[nodeId] = link.indexData; - } + return { data, details, resultElements, modelElements, nextLinkId, nextType, previousNodeId }; + } - // Check meta data - if ( - link.isWildcardIndexPattern === false && - (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) - ) { - rootIndexPattern = nextLinkId; - complete = true; - break; - } + /** + * Prepares the initial elements for incoming jobId + * @param jobId + */ + async getInitialElementsJobRoot(jobId: string): Promise<InitialElementsReturnType> { + const resultElements = []; + const modelElements = []; + const details: any = {}; + const data = await this.getAnalyticsData(jobId); + const nextLinkId = data?.source?.index[0]; + const nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX; + + const previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + resultElements.push({ + data: { + id: previousNodeId, + label: data.id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(data?.analysis), + isRoot: true, + }, + }); - if (link.meta?.created_by === 'data-frame-analytics') { - nextLinkId = link.meta.analytics; - nextType = JOB_MAP_NODE_TYPES.ANALYTICS; - } + details[previousNodeId] = data; - if (link.meta?.created_by === JOB_MAP_NODE_TYPES.TRANSFORM) { - nextLinkId = link.meta._transform?.transform; - nextType = JOB_MAP_NODE_TYPES.TRANSFORM; - } - } else if (isJobDataLinkReturnType(link) && link.isJob === true) { - data = link.jobData; - const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; - previousNodeId = nodeId; + const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(jobId); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } - result.elements.unshift({ - data: { - id: nodeId, - label: data.id, - type: JOB_MAP_NODE_TYPES.ANALYTICS, - analysisType: getAnalysisType(data?.analysis), - }, - }); - result.details[nodeId] = data; - nextLinkId = data?.source?.index[0]; - nextType = JOB_MAP_NODE_TYPES.INDEX; - - // Get inference model for analytics job and create model node - ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); - if (isAnalyticsMapNodeElement(modelElement)) { - modelElements.push(modelElement); - result.details[modelElement.data.id] = modelDetails; - } - if (isAnalyticsMapEdgeElement(edgeElement)) { - modelElements.push(edgeElement); + return { data, details, resultElements, modelElements, nextLinkId, nextType, previousNodeId }; + } + + /** + * Works backward from jobId or modelId to return related jobs, indices, models, and transforms + * @param jobId (optional) + * @param modelId (optional) + */ + async getAnalyticsMap({ + analyticsId, + modelId, + }: GetAnalyticsMapArgs): Promise<AnalyticsMapReturnType> { + const result: AnalyticsMapReturnType = { elements: [], details: {}, error: null }; + const modelElements: MapElements[] = []; + const indexPatternElements: MapElements[] = []; + + try { + await this.setInferenceModels(); + // Create first node for incoming analyticsId or modelId + let initialData: InitialElementsReturnType = {} as InitialElementsReturnType; + if (analyticsId !== undefined) { + initialData = await this.getInitialElementsJobRoot(analyticsId); + } else if (modelId !== undefined) { + initialData = await this.getInitialElementsModelRoot(modelId); + } + + const { + resultElements, + details: initialDetails, + modelElements: initialModelElements, + } = initialData; + + result.elements.push(...resultElements); + result.details = initialDetails; + modelElements.push(...initialModelElements); + + if (isCompleteInitialReturnType(initialData)) { + let { data, nextLinkId, nextType, previousNodeId } = initialData; + + let complete = false; + let link: NextLinkReturnType; + let count = 0; + let rootTransform; + let rootIndexPattern; + let modelElement; + let modelDetails; + let edgeElement; + + // Add a safeguard against infinite loops. + while (complete === false) { + count++; + if (count >= 100) { + break; } - } else if (isTransformLinkReturnType(link) && link.isTransform === true) { - data = link.transformData; - const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; - previousNodeId = nodeId; - rootTransform = data.dest.index; + try { + link = await this.getNextLink({ + id: nextLinkId, + type: nextType, + }); + } catch (error) { + result.error = error.message || 'Something went wrong'; + break; + } + // If it's index pattern, check meta data to see what to fetch next + if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { + if (link.isWildcardIndexPattern === true) { + // Create index nodes for each of the indices included in the index pattern then break + const { details, elements } = this.getIndexPatternElements( + link.indexData, + previousNodeId + ); + + indexPatternElements.push(...elements); + result.details = { ...result.details, ...details }; + complete = true; + } else { + const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.unshift({ + data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, + }); + result.details[nodeId] = link.indexData; + } - result.elements.unshift({ - data: { id: nodeId, label: data.id, type: JOB_MAP_NODE_TYPES.TRANSFORM }, - }); - result.details[nodeId] = data; - nextLinkId = data?.source?.index[0]; - nextType = JOB_MAP_NODE_TYPES.INDEX; - } - } // end while + // Check meta data + if ( + link.isWildcardIndexPattern === false && + (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) + ) { + rootIndexPattern = nextLinkId; + complete = true; + break; + } - // create edge elements - const elemLength = result.elements.length - 1; - for (let i = 0; i < elemLength; i++) { - const currentElem = result.elements[i]; - const nextElem = result.elements[i + 1]; - if ( - currentElem !== undefined && - nextElem !== undefined && - currentElem?.data?.id.includes('*') === false && - nextElem?.data?.id.includes('*') === false - ) { - result.elements.push({ - data: { - id: `${currentElem.data.id}~${nextElem.data.id}`, - source: currentElem.data.id, - target: nextElem.data.id, - }, - }); - } - } + if (link.meta?.created_by === 'data-frame-analytics') { + nextLinkId = link.meta.analytics; + nextType = JOB_MAP_NODE_TYPES.ANALYTICS; + } - // fetch all jobs associated with root transform if defined, otherwise check root index - if (rootTransform !== undefined || rootIndexPattern !== undefined) { - const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); - const jobs = analyticsJobs?.body?.data_frame_analytics || []; - const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; + if (link.meta?.created_by === JOB_MAP_NODE_TYPES.TRANSFORM) { + nextLinkId = link.meta._transform?.transform; + nextType = JOB_MAP_NODE_TYPES.TRANSFORM; + } + } else if (isJobDataLinkReturnType(link) && link.isJob === true) { + data = link.jobData; + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + previousNodeId = nodeId; - for (let i = 0; i < jobs.length; i++) { - if ( - jobs[i]?.source?.index[0] === comparator && - this.isDuplicateElement(jobs[i].id, result.elements) === false - ) { - const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; - result.elements.push({ + result.elements.unshift({ data: { id: nodeId, - label: jobs[i].id, + label: data.id, type: JOB_MAP_NODE_TYPES.ANALYTICS, - analysisType: getAnalysisType(jobs[i]?.analysis), - }, - }); - result.details[nodeId] = jobs[i]; - const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX}`; - result.elements.push({ - data: { - id: `${source}~${nodeId}`, - source, - target: nodeId, + analysisType: getAnalysisType(data?.analysis), }, }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + // Get inference model for analytics job and create model node - ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( - jobs[i].id - )); + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); if (isAnalyticsMapNodeElement(modelElement)) { modelElements.push(modelElement); result.details[modelElement.data.id] = modelDetails; @@ -399,12 +437,88 @@ export class AnalyticsManager { if (isAnalyticsMapEdgeElement(edgeElement)) { modelElements.push(edgeElement); } + } else if (isTransformLinkReturnType(link) && link.isTransform === true) { + data = link.transformData; + + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; + previousNodeId = nodeId; + rootTransform = data.dest.index; + + result.elements.unshift({ + data: { id: nodeId, label: data.id, type: JOB_MAP_NODE_TYPES.TRANSFORM }, + }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + } + } // end while + + // create edge elements + const elemLength = result.elements.length - 1; + for (let i = 0; i < elemLength; i++) { + const currentElem = result.elements[i]; + const nextElem = result.elements[i + 1]; + if ( + currentElem !== undefined && + nextElem !== undefined && + currentElem?.data?.id.includes('*') === false && + nextElem?.data?.id.includes('*') === false + ) { + result.elements.push({ + data: { + id: `${currentElem.data.id}~${nextElem.data.id}`, + source: currentElem.data.id, + target: nextElem.data.id, + }, + }); + } + } + + // fetch all jobs associated with root transform if defined, otherwise check root index + if (rootTransform !== undefined || rootIndexPattern !== undefined) { + const jobs = await this.getAnalyticsData(); + const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; + + for (let i = 0; i < jobs.length; i++) { + if ( + jobs[i]?.source?.index[0] === comparator && + this.isDuplicateElement(jobs[i].id, result.elements) === false + ) { + const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + result.elements.push({ + data: { + id: nodeId, + label: jobs[i].id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(jobs[i]?.analysis), + }, + }); + result.details[nodeId] = jobs[i]; + const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.push({ + data: { + id: `${source}~${nodeId}`, + source, + target: nodeId, + }, + }); + // Get inference model for analytics job and create model node + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + jobs[i].id + )); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } + } } } } // Include model and index pattern nodes in result elements now that all other nodes have been created result.elements.push(...modelElements, ...indexPatternElements); - return result; } catch (error) { result.error = error.message || 'An error occurred fetching map'; @@ -412,56 +526,64 @@ export class AnalyticsManager { } } - async extendAnalyticsMapForAnalyticsJob(analyticsId: string): Promise<AnalyticsMapReturnType> { - const result: any = { elements: [], details: {}, error: null }; - + async extendAnalyticsMapForAnalyticsJob({ + analyticsId, + index, + }: ExtendAnalyticsMapArgs): Promise<AnalyticsMapReturnType> { + const result: AnalyticsMapReturnType = { elements: [], details: {}, error: null }; try { await this.setInferenceModels(); + const jobs = await this.getAnalyticsData(); + let rootIndex; + let rootIndexNodeId; + + if (analyticsId !== undefined) { + const jobData = await this.getAnalyticsData(analyticsId); + const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + rootIndex = Array.isArray(jobData?.dest?.index) + ? jobData?.dest?.index[0] + : jobData?.dest?.index; + rootIndexNodeId = `${rootIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; + + // Fetch inference model for incoming job id and add node and edge + const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + analyticsId + ); + if (isAnalyticsMapNodeElement(modelElement)) { + result.elements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + result.elements.push(edgeElement); + } - const jobData = await this.getAnalyticsJobData(analyticsId); - const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; - const destIndex = Array.isArray(jobData?.dest?.index) - ? jobData?.dest?.index[0] - : jobData?.dest?.index; - const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; - const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); - const jobs = analyticsJobs?.body?.data_frame_analytics || []; - - // Fetch inference model for incoming job id and add node and edge - const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( - analyticsId - ); - if (isAnalyticsMapNodeElement(modelElement)) { - result.elements.push(modelElement); - result.details[modelElement.data.id] = modelDetails; - } - if (isAnalyticsMapEdgeElement(edgeElement)) { - result.elements.push(edgeElement); + // If rootIndex node has not been created, create it + const rootIndexDetails = await this.getIndexData(rootIndex); + result.elements.push({ + data: { + id: rootIndexNodeId, + label: rootIndex, + type: JOB_MAP_NODE_TYPES.INDEX, + }, + }); + result.details[rootIndexNodeId] = rootIndexDetails; + + // Connect incoming job to rootIndex + result.elements.push({ + data: { + id: `${currentJobNodeId}~${rootIndexNodeId}`, + source: currentJobNodeId, + target: rootIndexNodeId, + }, + }); + } else { + rootIndex = index; + rootIndexNodeId = `${rootIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; } - // If destIndex node has not been created, create it - const destIndexDetails = await this.getIndexData(destIndex); - result.elements.push({ - data: { - id: destIndexNodeId, - label: destIndex, - type: JOB_MAP_NODE_TYPES.INDEX, - }, - }); - result.details[destIndexNodeId] = destIndexDetails; - - // Connect incoming job to destIndex - result.elements.push({ - data: { - id: `${currentJobNodeId}~${destIndexNodeId}`, - source: currentJobNodeId, - target: destIndexNodeId, - }, - }); - for (let i = 0; i < jobs.length; i++) { if ( - jobs[i]?.source?.index[0] === destIndex && + jobs[i]?.source?.index[0] === rootIndex && this.isDuplicateElement(jobs[i].id, result.elements) === false ) { // Create node for associated job @@ -478,8 +600,8 @@ export class AnalyticsManager { result.elements.push({ data: { - id: `${destIndexNodeId}~${nodeId}`, - source: destIndexNodeId, + id: `${rootIndexNodeId}~${nodeId}`, + source: rootIndexNodeId, target: nodeId, }, }); diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts index 5d6cec8cdfa61..e34d68ec7840c 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts @@ -4,6 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import { JobMapNodeTypes } from '../../../common/constants/data_frame_analytics'; + +interface AnalyticsMapArg { + analyticsId: string; +} +interface GetAnalyticsJobIdArg extends AnalyticsMapArg { + modelId?: never; +} +interface GetAnalyticsModelIdArg { + analyticsId?: never; + modelId: string; +} +interface ExtendAnalyticsJobIdArg extends AnalyticsMapArg { + index?: never; +} +interface ExtendAnalyticsIndexArg { + analyticsId?: never; + index: string; +} + +export type GetAnalyticsMapArgs = GetAnalyticsJobIdArg | GetAnalyticsModelIdArg; +export type ExtendAnalyticsMapArgs = ExtendAnalyticsJobIdArg | ExtendAnalyticsIndexArg; + export interface IndexPatternLinkReturnType { isWildcardIndexPattern: boolean; isIndexPattern: boolean; @@ -26,9 +49,27 @@ export type NextLinkReturnType = export type MapElements = AnalyticsMapNodeElement | AnalyticsMapEdgeElement; export interface AnalyticsMapReturnType { elements: MapElements[]; - details: object; // transform, job, or index details + details: Record<string, any>; // transform, job, or index details error: null | any; } + +interface BasicInitialElementsReturnType { + data: any; + details: object; + resultElements: MapElements[]; + modelElements: MapElements[]; +} + +export interface InitialElementsReturnType extends BasicInitialElementsReturnType { + nextLinkId?: string; + nextType?: JobMapNodeTypes; + previousNodeId?: string; +} +interface CompleteInitialElementsReturnType extends BasicInitialElementsReturnType { + nextLinkId: string; + nextType: JobMapNodeTypes; + previousNodeId: string; +} export interface AnalyticsMapNodeElement { data: { id: string; @@ -44,6 +85,16 @@ export interface AnalyticsMapEdgeElement { target: string; }; } +export const isCompleteInitialReturnType = (arg: any): arg is CompleteInitialElementsReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return ( + keys.length > 0 && + keys.includes('nextLinkId') && + keys.includes('nextType') && + keys.includes('previousNodeId') + ); +}; export const isAnalyticsMapNodeElement = (arg: any): arg is AnalyticsMapNodeElement => { if (typeof arg !== 'object' || arg === null) return false; const keys = Object.keys(arg); diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 8d6dd692cc130..c157ae9e8200f 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -17,6 +17,7 @@ "UpdateDataFrameAnalytics", "DeleteDataFrameAnalytics", "JobsExist", + "GetDataFrameAnalyticsIdMap", "DataVisualizer", "GetOverallStats", diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 8e00ae7068403..0abba7a429aea 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -8,6 +8,7 @@ import { RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; import { RouteInitialization } from '../types'; +import { JOB_MAP_NODE_TYPES } from '../../common/constants/data_frame_analytics'; import { dataAnalyticsJobConfigSchema, dataAnalyticsJobUpdateSchema, @@ -19,6 +20,7 @@ import { deleteDataFrameAnalyticsJobSchema, jobsExistSchema, } from './schemas/data_analytics_schema'; +import { GetAnalyticsMapArgs, ExtendAnalyticsMapArgs } from '../models/data_frame_analytics/types'; import { IndexPatternHandler } from '../models/data_frame_analytics/index_patterns'; import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manager'; import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics'; @@ -36,14 +38,22 @@ function deleteDestIndexPatternById(context: RequestHandlerContext, indexPattern return iph.deleteIndexPatternById(indexPatternId); } -function getAnalyticsMap(mlClient: MlClient, client: IScopedClusterClient, analyticsId: string) { +function getAnalyticsMap( + mlClient: MlClient, + client: IScopedClusterClient, + idOptions: GetAnalyticsMapArgs +) { const analytics = new AnalyticsManager(mlClient, client.asInternalUser); - return analytics.getAnalyticsMap(analyticsId); + return analytics.getAnalyticsMap(idOptions); } -function getExtendedMap(mlClient: MlClient, client: IScopedClusterClient, analyticsId: string) { +function getExtendedMap( + mlClient: MlClient, + client: IScopedClusterClient, + idOptions: ExtendAnalyticsMapArgs +) { const analytics = new AnalyticsManager(mlClient, client.asInternalUser); - return analytics.extendAnalyticsMapForAnalyticsJob(analyticsId); + return analytics.extendAnalyticsMapForAnalyticsJob(idOptions); } /** @@ -633,10 +643,20 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout try { const { analyticsId } = request.params; const treatAsRoot = request.query?.treatAsRoot; - const caller = - treatAsRoot === 'true' || treatAsRoot === true ? getExtendedMap : getAnalyticsMap; + const type = request.query?.type; - const results = await caller(mlClient, client, analyticsId); + let results; + if (treatAsRoot === 'true' || treatAsRoot === true) { + results = await getExtendedMap(mlClient, client, { + analyticsId: type !== JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined, + index: type === JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined, + }); + } else { + results = await getAnalyticsMap(mlClient, client, { + analyticsId: type !== JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined, + modelId: type === JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined, + }); + } return response.ok({ body: results, diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index d8226b70eb2c3..cf52d1cb27433 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -89,5 +89,5 @@ export const jobsExistSchema = schema.object({ }); export const analyticsMapQuerySchema = schema.maybe( - schema.object({ treatAsRoot: schema.maybe(schema.any()) }) + schema.object({ treatAsRoot: schema.maybe(schema.any()), type: schema.maybe(schema.string()) }) ); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js deleted file mode 100644 index 5d8af8d71b7fc..0000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ /dev/null @@ -1,275 +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 { defaultsDeep, uniq, compact } from 'lodash'; -import { ServiceStatusLevels } from '../../../../../src/core/server'; -import { - TELEMETRY_COLLECTION_INTERVAL, - KIBANA_STATS_TYPE_MONITORING, -} from '../../common/constants'; - -import { sendBulkPayload, monitoringBulk } from './lib'; - -/* - * Handles internal Kibana stats collection and uploading data to Monitoring - * bulk endpoint. - * - * NOTE: internal collection will be removed in 7.0 - * - * Depends on - * - 'monitoring.kibana.collection.enabled' config - * - monitoring enabled in ES (checked against xpack_main.info license info change) - * The dependencies are handled upstream - * - Ops Events - essentially Kibana's /api/status - * - Usage Stats - essentially Kibana's /api/stats - * - Kibana Settings - select uiSettings - * @param {Object} server HapiJS server instance - * @param {Object} xpackInfo server.plugins.xpack_main.info object - */ -export class BulkUploader { - constructor({ log, interval, elasticsearch, statusGetter$, kibanaStats }) { - if (typeof interval !== 'number') { - throw new Error('interval number of milliseconds is required'); - } - - this._timer = null; - // Hold sending and fetching usage until monitoring.bulk is successful. This means that we - // send usage data on the second tick. But would save a lot of bandwidth fetching usage on - // every tick when ES is failing or monitoring is disabled. - this._holdSendingUsage = false; - this._interval = interval; - this._lastFetchUsageTime = null; - // Limit sending and fetching usage to once per day once usage is successfully stored - // into the monitoring indices. - this._usageInterval = TELEMETRY_COLLECTION_INTERVAL; - this._log = log; - - this._cluster = elasticsearch.legacy.createClient('admin', { - plugins: [monitoringBulk], - }); - - this.kibanaStats = kibanaStats; - - this.kibanaStatus = null; - this.kibanaStatusGetter$ = statusGetter$.subscribe((nextStatus) => { - this.kibanaStatus = nextStatus.level; - }); - } - - filterCollectorSet(usageCollection) { - const successfulUploadInLastDay = - this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now(); - - return usageCollection.getFilteredCollectorSet((c) => { - // this is internal bulk upload, so filter out API-only collectors - if (c.ignoreForInternalUploader) { - return false; - } - // Only collect usage data at the same interval as telemetry would (default to once a day) - if (usageCollection.isUsageCollector(c)) { - if (this._holdSendingUsage) { - return false; - } - if (successfulUploadInLastDay) { - return false; - } - } - - return true; - }); - } - - /* - * Start the interval timer - * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval - * @return undefined - */ - start(usageCollection) { - this._log.info('Starting monitoring stats collection'); - - if (this._timer) { - clearInterval(this._timer); - } else { - this._fetchAndUpload(this.filterCollectorSet(usageCollection)); // initial fetch - } - - this._timer = setInterval(() => { - this._fetchAndUpload(this.filterCollectorSet(usageCollection)); - }, this._interval); - } - - /* - * start() and stop() are lifecycle event handlers for - * xpackMainPlugin license changes - * @param {String} logPrefix help give context to the reason for stopping - */ - stop(logPrefix) { - clearInterval(this._timer); - this._timer = null; - - const prefix = logPrefix ? logPrefix + ':' : ''; - this._log.info(prefix + 'Monitoring stats collection is stopped'); - } - - handleNotEnabled() { - this.stop('Monitoring status upload endpoint is not enabled in Elasticsearch'); - } - handleConnectionLost() { - this.stop('Connection issue detected'); - } - - /* - * @param {usageCollection} usageCollection - * @return {Promise} - resolves to undefined - */ - async _fetchAndUpload(usageCollection) { - const collectorsReady = await usageCollection.areAllCollectorsReady(); - const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); - if (!collectorsReady) { - this._log.debug('Skipping bulk uploading because not all collectors are ready'); - if (hasUsageCollectors) { - this._lastFetchUsageTime = null; - this._log.debug('Resetting lastFetchWithUsage because not all collectors are ready'); - } - return; - } - - const data = await usageCollection.bulkFetch(this._cluster.callAsInternalUser); - const payload = this.toBulkUploadFormat(compact(data), usageCollection); - if (payload && payload.length > 0) { - try { - this._log.debug(`Uploading bulk stats payload to the local cluster`); - const result = await this._onPayload(payload); - const sendSuccessful = !result.ignored && !result.errors; - if (!sendSuccessful && hasUsageCollectors) { - this._lastFetchUsageTime = null; - this._holdSendingUsage = true; - this._log.debug( - 'Resetting lastFetchWithUsage because uploading to the cluster was not successful.' - ); - } - - if (sendSuccessful) { - this._holdSendingUsage = false; - if (hasUsageCollectors) { - this._lastFetchUsageTime = Date.now(); - } - } - this._log.debug(`Uploaded bulk stats payload to the local cluster`); - } catch (err) { - this._log.warn(err.stack); - this._log.warn(`Unable to bulk upload the stats payload to the local cluster`); - } - } else { - this._log.debug(`Skipping bulk uploading of an empty stats payload`); - } - } - - async _onPayload(payload) { - return await sendBulkPayload(this._cluster, this._interval, payload, this._log); - } - - getConvertedKibanaStatuss() { - if (this.kibanaStatus === ServiceStatusLevels.available) { - return 'green'; - } - if (this.kibanaStatus === ServiceStatusLevels.critical) { - return 'red'; - } - if (this.kibanaStatus === ServiceStatusLevels.degraded) { - return 'yellow'; - } - return 'unknown'; - } - - getKibanaStats(type) { - const stats = { - ...this.kibanaStats, - status: this.getConvertedKibanaStatuss(), - }; - - if (type === KIBANA_STATS_TYPE_MONITORING) { - delete stats.port; - delete stats.locale; - } - - return stats; - } - - /* - * Bulk stats are transformed into a bulk upload format - * Non-legacy transformation is done in CollectorSet.toApiStats - * - * Example: - * Before: - * [ - * { - * "type": "kibana_stats", - * "result": { - * "process": { ... }, - * "requests": { ... }, - * ... - * } - * }, - * ] - * - * After: - * [ - * { - * "index": { - * "_type": "kibana_stats" - * } - * }, - * { - * "kibana": { - * "host": "localhost", - * "uuid": "d619c5d1-4315-4f35-b69d-a3ac805489fb", - * "version": "7.0.0-alpha1", - * ... - * }, - * "process": { ... }, - * "requests": { ... }, - * ... - * } - * ] - */ - toBulkUploadFormat(rawData, usageCollection) { - if (rawData.length === 0) { - return []; - } - - // convert the raw data to a nested object by taking each payload through - // its formatter, organizing it per-type - const typesNested = rawData.reduce((accum, { type, result }) => { - const { type: uploadType, payload: uploadData } = usageCollection - .getCollectorByType(type) - .formatForBulkUpload(result); - return defaultsDeep(accum, { [uploadType]: uploadData }); - }, {}); - // convert the nested object into a flat array, with each payload prefixed - // with an 'index' instruction, for bulk upload - const flat = Object.keys(typesNested).reduce((accum, type) => { - return [ - ...accum, - { index: { _type: type } }, - { - kibana: this.getKibanaStats(type), - ...typesNested[type], - }, - ]; - }, []); - - return flat; - } - - static checkPayloadTypesUnique(payload) { - const ids = payload.map((item) => item[0].index._type); - const uniques = uniq(ids); - if (ids.length !== uniques.length) { - throw new Error('Duplicate collector type identifiers found in payload! ' + ids.join(',')); - } - } -} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts new file mode 100644 index 0000000000000..e17d3e58e859c --- /dev/null +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable, Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; +import moment from 'moment'; +import { + ElasticsearchServiceSetup, + ILegacyCustomClusterClient, + Logger, + OpsMetrics, + ServiceStatus, + ServiceStatusLevel, + ServiceStatusLevels, +} from '../../../../../src/core/server'; +import { KIBANA_STATS_TYPE_MONITORING, KIBANA_SETTINGS_TYPE } from '../../common/constants'; + +import { sendBulkPayload, monitoringBulk } from './lib'; +import { getKibanaSettings } from './collectors'; +import { MonitoringConfig } from '../config'; + +export interface BulkUploaderOptions { + log: Logger; + config: MonitoringConfig; + interval: number; + elasticsearch: ElasticsearchServiceSetup; + statusGetter$: Observable<ServiceStatus>; + opsMetrics$: Observable<OpsMetrics>; + kibanaStats: KibanaStats; +} + +export interface KibanaStats { + uuid: string; + name: string; + index: string; + host: string; + locale: string; + port: string; + transport_address: string; + version: string; + snapshot: boolean; +} + +/* + * Handles internal Kibana stats collection and uploading data to Monitoring + * bulk endpoint. + * + * NOTE: internal collection will be removed in 7.0 + * + * Depends on + * - 'monitoring.kibana.collection.enabled' config + * - monitoring enabled in ES (checked against xpack_main.info license info change) + * The dependencies are handled upstream + * - Ops Events - essentially Kibana's /api/status + * - Usage Stats - essentially Kibana's /api/stats + * - Kibana Settings - select uiSettings + * @param {Object} server HapiJS server instance + * @param {Object} xpackInfo server.plugins.xpack_main.info object + */ +export class BulkUploader { + private readonly _log: Logger; + private readonly _cluster: ILegacyCustomClusterClient; + private readonly kibanaStats: KibanaStats; + private readonly kibanaStatusGetter$: Subscription; + private readonly opsMetrics$: Observable<OpsMetrics>; + private kibanaStatus: ServiceStatusLevel | null; + private _timer: NodeJS.Timer | null; + private readonly _interval: number; + private readonly config: MonitoringConfig; + constructor({ + log, + config, + interval, + elasticsearch, + statusGetter$, + opsMetrics$, + kibanaStats, + }: BulkUploaderOptions) { + if (typeof interval !== 'number') { + throw new Error('interval number of milliseconds is required'); + } + + this.opsMetrics$ = opsMetrics$; + this.config = config; + + this._timer = null; + this._interval = interval; + this._log = log; + + this._cluster = elasticsearch.legacy.createClient('admin', { + plugins: [monitoringBulk], + }); + + this.kibanaStats = kibanaStats; + + this.kibanaStatus = null; + this.kibanaStatusGetter$ = statusGetter$.subscribe((nextStatus) => { + this.kibanaStatus = nextStatus.level; + }); + } + + /* + * Start the interval timer + * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval + * @return undefined + */ + public start() { + this._log.info('Starting monitoring stats collection'); + + if (this._timer) { + clearInterval(this._timer); + } else { + this._fetchAndUpload(); // initial fetch + } + + this._timer = setInterval(() => { + this._fetchAndUpload(); + }, this._interval); + } + + /* + * start() and stop() are lifecycle event handlers for + * xpackMainPlugin license changes + * @param {String} logPrefix help give context to the reason for stopping + */ + public stop(logPrefix?: string) { + if (this._timer) clearInterval(this._timer); + this._timer = null; + + this.kibanaStatusGetter$.unsubscribe(); + this._cluster.close(); + + const prefix = logPrefix ? logPrefix + ':' : ''; + this._log.info(prefix + 'Monitoring stats collection is stopped'); + } + + public handleNotEnabled() { + this.stop('Monitoring status upload endpoint is not enabled in Elasticsearch'); + } + public handleConnectionLost() { + this.stop('Connection issue detected'); + } + + /** + * Retrieves the OpsMetrics in the same format as the `kibana_stats` collector + * @private + */ + private async getOpsMetrics() { + const { + process: { pid, ...process }, + collected_at: collectedAt, + requests: { statusCodes, ...requests }, + ...lastMetrics + } = await this.opsMetrics$.pipe(take(1)).toPromise(); + return { + ...lastMetrics, + process, + requests, + response_times: { + average: lastMetrics.response_times.avg_in_millis, + max: lastMetrics.response_times.max_in_millis, + }, + timestamp: moment.utc(collectedAt).toISOString(), + }; + } + + private async _fetchAndUpload() { + const data = await Promise.all([ + { type: KIBANA_STATS_TYPE_MONITORING, result: await this.getOpsMetrics() }, + { type: KIBANA_SETTINGS_TYPE, result: await getKibanaSettings(this._log, this.config) }, + ]); + + const payload = this.toBulkUploadFormat(data); + if (payload && payload.length > 0) { + try { + this._log.debug(`Uploading bulk stats payload to the local cluster`); + await this._onPayload(payload); + this._log.debug(`Uploaded bulk stats payload to the local cluster`); + } catch (err) { + this._log.warn(err.stack); + this._log.warn(`Unable to bulk upload the stats payload to the local cluster`); + } + } else { + this._log.debug(`Skipping bulk uploading of an empty stats payload`); + } + } + + private async _onPayload(payload: object[]) { + return await sendBulkPayload(this._cluster, this._interval, payload); + } + + private getConvertedKibanaStatus() { + if (this.kibanaStatus === ServiceStatusLevels.available) { + return 'green'; + } + if (this.kibanaStatus === ServiceStatusLevels.critical) { + return 'red'; + } + if (this.kibanaStatus === ServiceStatusLevels.degraded) { + return 'yellow'; + } + return 'unknown'; + } + + public getKibanaStats(type?: string) { + const stats = { + ...this.kibanaStats, + status: this.getConvertedKibanaStatus(), + }; + + if (type === KIBANA_STATS_TYPE_MONITORING) { + // Do not report the keys `port` and `locale` + const { port, locale, ...rest } = stats; + return rest; + } + + return stats; + } + + /* + * Bulk stats are transformed into a bulk upload format + * Non-legacy transformation is done in CollectorSet.toApiStats + * + * Example: + * Before: + * [ + * { + * "type": "kibana_stats", + * "result": { + * "process": { ... }, + * "requests": { ... }, + * ... + * } + * }, + * ] + * + * After: + * [ + * { + * "index": { + * "_type": "kibana_stats" + * } + * }, + * { + * "kibana": { + * "host": "localhost", + * "uuid": "d619c5d1-4315-4f35-b69d-a3ac805489fb", + * "version": "7.0.0-alpha1", + * ... + * }, + * "process": { ... }, + * "requests": { ... }, + * ... + * } + * ] + */ + private toBulkUploadFormat(rawData: Array<{ type: string; result: any }>) { + // convert the raw data into a flat array, with each payload prefixed + // with an 'index' instruction, for bulk upload + return rawData.reduce((accum, { type, result }) => { + return [ + ...accum, + { index: { _type: type } }, + { + kibana: this.getKibanaStats(type), + ...result, + }, + ]; + }, [] as object[]); + } +} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 2b81f1078ad0a..858c50790fc2e 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from 'src/core/server'; import { Collector, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_SETTINGS_TYPE } from '../../../common/constants'; @@ -51,6 +52,37 @@ export interface KibanaSettingsCollectorExtraOptions { export type KibanaSettingsCollector = Collector<EmailSettingData | undefined> & KibanaSettingsCollectorExtraOptions; +export function getEmailValueStructure(email: string | null) { + return { + xpack: { + default_admin_email: email, + }, + }; +} + +export async function getKibanaSettings(logger: Logger, config: MonitoringConfig) { + let kibanaSettingsData; + const defaultAdminEmail = await checkForEmailValue(config); + + // skip everything if defaultAdminEmail === undefined + if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) { + kibanaSettingsData = getEmailValueStructure(defaultAdminEmail); + logger.debug( + `[${defaultAdminEmail}] default admin email setting found, sending [${KIBANA_SETTINGS_TYPE}] monitoring document.` + ); + } else { + logger.debug( + `not sending [${KIBANA_SETTINGS_TYPE}] monitoring document because [${defaultAdminEmail}] is null or invalid.` + ); + } + + // remember the current email so that we can mark it as successful if the bulk does not error out + shouldUseNull = !!defaultAdminEmail; + + // returns undefined if there was no result + return kibanaSettingsData; +} + export function getSettingsCollector( usageCollection: UsageCollectionSetup, config: MonitoringConfig @@ -69,33 +101,10 @@ export function getSettingsCollector( }, }, async fetch() { - let kibanaSettingsData; - const defaultAdminEmail = await checkForEmailValue(config); - - // skip everything if defaultAdminEmail === undefined - if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) { - kibanaSettingsData = this.getEmailValueStructure(defaultAdminEmail); - this.log.debug( - `[${defaultAdminEmail}] default admin email setting found, sending [${KIBANA_SETTINGS_TYPE}] monitoring document.` - ); - } else { - this.log.debug( - `not sending [${KIBANA_SETTINGS_TYPE}] monitoring document because [${defaultAdminEmail}] is null or invalid.` - ); - } - - // remember the current email so that we can mark it as successful if the bulk does not error out - shouldUseNull = !!defaultAdminEmail; - - // returns undefined if there was no result - return kibanaSettingsData; + return getKibanaSettings(this.log, config); }, getEmailValueStructure(email: string | null) { - return { - xpack: { - default_admin_email: email, - }, - }; + return getEmailValueStructure(email); }, }); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts index 25e243656898c..5fb1583a5c0db 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts @@ -10,7 +10,7 @@ import { getSettingsCollector } from './get_settings_collector'; import { getMonitoringUsageCollector } from './get_usage_collector'; import { MonitoringConfig } from '../../config'; -export { KibanaSettingsCollector } from './get_settings_collector'; +export { KibanaSettingsCollector, getKibanaSettings } from './get_settings_collector'; export function registerCollectors( usageCollection: UsageCollectionSetup, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/index.js b/x-pack/plugins/monitoring/server/kibana_monitoring/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/kibana_monitoring/index.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/index.ts diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/init.js b/x-pack/plugins/monitoring/server/kibana_monitoring/init.ts similarity index 76% rename from x-pack/plugins/monitoring/server/kibana_monitoring/init.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/init.ts index 79aafb8f361f3..c8c5fabb65db0 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/init.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/init.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BulkUploader } from './bulk_uploader'; +import { BulkUploader, BulkUploaderOptions } from './bulk_uploader'; + +export type InitBulkUploaderOptions = Omit<BulkUploaderOptions, 'interval'>; /** * Initialize different types of Kibana Monitoring @@ -15,7 +17,7 @@ import { BulkUploader } from './bulk_uploader'; * @param {Object} kbnServer manager of Kibana services - see `src/legacy/server/kbn_server` in Kibana core * @param {Object} server HapiJS server instance */ -export function initBulkUploader({ config, ...params }) { +export function initBulkUploader({ config, ...params }: InitBulkUploaderOptions) { const interval = config.kibana.collection.interval; return new BulkUploader({ interval, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.js b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.ts similarity index 96% rename from x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.ts index c5fdd29d4306d..a6c5583329861 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.ts @@ -5,4 +5,5 @@ */ export { sendBulkPayload } from './send_bulk_payload'; +// @ts-ignore export { monitoringBulk } from './monitoring_bulk'; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.ts similarity index 78% rename from x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.ts index 66799e4aa651a..78d689fe9f182 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.ts @@ -3,12 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ILegacyClusterClient } from 'src/core/server'; import { MONITORING_SYSTEM_API_VERSION, KIBANA_SYSTEM_ID } from '../../../common/constants'; /* * Send the Kibana usage data to the ES Monitoring Bulk endpoint */ -export async function sendBulkPayload(cluster, interval, payload) { +export async function sendBulkPayload( + cluster: ILegacyClusterClient, + interval: number, + payload: object[] +) { return cluster.callAsInternalUser('monitoring.bulk', { system_id: KIBANA_SYSTEM_ID, system_api_version: MONITORING_SYSTEM_API_VERSION, diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 3fc494d6c3706..b376fc2eec60b 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.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 { coreMock } from 'src/core/server/mocks'; import { Plugin } from './plugin'; import { combineLatest } from 'rxjs'; import { AlertsFactory } from './alerts'; @@ -53,31 +54,9 @@ describe('Monitoring plugin', () => { }, }; - const coreSetup = { - http: { - createRouter: jest.fn(), - getServerInfo: jest.fn().mockImplementation(() => ({ - port: 5601, - })), - basePath: { - serverBasePath: '', - }, - }, - elasticsearch: { - legacy: { - client: {}, - createClient: jest.fn(), - }, - }, - status: { - overall$: { - subscribe: jest.fn(), - }, - }, - savedObjects: { - registerType: jest.fn(), - }, - }; + const coreSetup = coreMock.createSetup(); + coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); + coreSetup.status.overall$.subscribe = jest.fn(); const setupPlugins = { usageCollection: { @@ -124,7 +103,7 @@ describe('Monitoring plugin', () => { it('always create the bulk uploader', async () => { const plugin = new Plugin(initializerContext as any); - await plugin.setup(coreSetup as any, setupPlugins as any); + await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 8a8e6a867c2e2..af5e1fca76308 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -30,11 +30,8 @@ import { SAVED_OBJECT_TELEMETRY, } from '../common/constants'; import { MonitoringConfig, createConfig, configSchema } from './config'; -// @ts-ignore import { requireUIRoutes } from './routes'; -// @ts-ignore import { initBulkUploader } from './kibana_monitoring'; -// @ts-ignore import { initInfraSource } from './lib/logs/init_infra_source'; import { mbSafeQuery } from './lib/mb_safe_query'; import { instantiateClient } from './es_client/instantiate_client'; @@ -73,7 +70,7 @@ export class Plugin { private licenseService = {} as MonitoringLicenseService; private monitoringCore = {} as MonitoringCore; private legacyShimDependencies = {} as LegacyShimDependencies; - private bulkUploader: IBulkUploader = {} as IBulkUploader; + private bulkUploader: IBulkUploader | undefined; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; @@ -170,6 +167,7 @@ export class Plugin { elasticsearch: core.elasticsearch, config, log: kibanaMonitoringLog, + opsMetrics$: core.metrics.getOpsMetrics$(), statusGetter$: core.status.overall$, kibanaStats: { uuid: this.initializerContext.env.instanceUuid, @@ -196,7 +194,7 @@ export class Plugin { const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable && mainMonitoring.isEnabled; if (monitoringBulkEnabled) { - bulkUploader.start(plugins.usageCollection); + bulkUploader.start(); } else { bulkUploader.handleNotEnabled(); } @@ -237,7 +235,7 @@ export class Plugin { return { // OSS stats api needs to call this in order to centralize how // we fetch kibana specific stats - getKibanaStats: () => this.bulkUploader.getKibanaStats(), + getKibanaStats: () => bulkUploader.getKibanaStats(), }; } @@ -250,6 +248,7 @@ export class Plugin { if (this.licenseService) { this.licenseService.stop(); } + this.bulkUploader?.stop(); } registerPluginInUI(plugins: PluginsSetup) { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index b25daced50b73..a5d7051105797 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -72,6 +72,7 @@ export interface LegacyShimDependencies { export interface IBulkUploader { getKibanaStats: () => any; + stop: () => void; } export interface LegacyRequest { diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index dfe7280b717a3..2c08354c9111f 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -11,9 +11,25 @@ import { ObservabilityPluginSetupDeps } from '../plugin'; import { renderApp } from './'; describe('renderApp', () => { + const originalConsole = global.console; + beforeAll(() => { + // mocks console to avoid poluting the test output + global.console = ({ error: jest.fn() } as unknown) as typeof console; + }); + + afterAll(() => { + global.console = originalConsole; + }); it('renders', async () => { const plugins = ({ usageCollection: { reportUiStats: () => {} }, + data: { + query: { + timefilter: { + timefilter: { setTime: jest.fn(), getTime: jest.fn().mockImplementation(() => ({})) }, + }, + }, + }, } as unknown) as ObservabilityPluginSetupDeps; const core = ({ application: { currentAppId$: new Observable(), navigateToUrl: () => {} }, diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 585a45cf5279c..ea84a417c20eb 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -17,6 +17,7 @@ import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; import { ObservabilityPluginSetupDeps } from '../plugin'; +import { HasDataContextProvider } from '../context/has_data_context'; import { Breadcrumbs, routes } from '../routes'; const observabilityLabelBreadcrumb = { @@ -46,8 +47,8 @@ function App() { core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb)); }, [core, breadcrumb]); - const { query, path: pathParams } = useRouteParams(route.params); - return route.handler({ query, path: pathParams }); + const params = useRouteParams(path); + return route.handler(params); }; return <Route key={path} path={path} exact={true} component={Wrapper} />; })} @@ -79,7 +80,9 @@ export const renderApp = ( <EuiThemeProvider darkMode={isDarkMode}> <i18nCore.Context> <RedirectAppLinks application={core.application}> - <App /> + <HasDataContextProvider> + <App /> + </HasDataContextProvider> </RedirectAppLinks> </i18nCore.Context> </EuiThemeProvider> diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/empty_section.test.tsx similarity index 96% rename from x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx rename to x-pack/plugins/observability/public/components/app/empty_sections/empty_section.test.tsx index 6a05749df6d7a..22867dde83a00 100644 --- a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_sections/empty_section.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { ISection } from '../../../typings/section'; import { render } from '../../../utils/test_helper'; -import { EmptySection } from './'; +import { EmptySection } from './empty_section'; describe('EmptySection', () => { it('renders without action button', () => { diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/empty_section.tsx similarity index 100% rename from x-pack/plugins/observability/public/components/app/empty_section/index.tsx rename to x-pack/plugins/observability/public/components/app/empty_sections/empty_section.tsx diff --git a/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx new file mode 100644 index 0000000000000..34522ef95e27b --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { Alert } from '../../../../../alerts/common'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useHasData } from '../../../hooks/use_has_data'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; +import { getEmptySections } from '../../../pages/overview/empty_section'; +import { UXHasDataResponse } from '../../../typings'; +import { EmptySection } from './empty_section'; + +export function EmptySections() { + const { core } = usePluginContext(); + const theme = useContext(ThemeContext); + const { hasData } = useHasData(); + + const appEmptySections = getEmptySections({ core }).filter(({ id }) => { + if (id === 'alert') { + const { status, hasData: alerts } = hasData.alert || {}; + return ( + status === FETCH_STATUS.FAILURE || + (status === FETCH_STATUS.SUCCESS && (alerts as Alert[]).length === 0) + ); + } else { + const app = hasData[id]; + if (app) { + const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData; + return app.status === FETCH_STATUS.FAILURE || !_hasData; + } + } + return false; + }); + return ( + <EuiFlexItem> + <EuiSpacer size="s" /> + <EuiFlexGrid + columns={ + // when more than 2 empty sections are available show them on 2 columns, otherwise 1 + appEmptySections.length > 2 ? 2 : 1 + } + gutterSize="s" + > + {appEmptySections.map((app) => { + return ( + <EuiFlexItem + key={app.id} + style={{ + border: `1px dashed ${theme.eui.euiBorderColor}`, + borderRadius: '4px', + }} + > + <EmptySection section={app} /> + </EuiFlexItem> + ); + })} + </EuiFlexGrid> + </EuiFlexItem> + ); +} 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 7b9d7276dd1c5..9fdc59d61257e 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 @@ -8,25 +8,59 @@ import * as fetcherHook from '../../../../hooks/use_fetcher'; import { render } from '../../../../utils/test_helper'; import { APMSection } from './'; import { response } from './mock_data/apm.mock'; -import moment from 'moment'; +import * as hasDataHook from '../../../../hooks/use_has_data'; +import * as pluginContext from '../../../../hooks/use_plugin_context'; +import { HasDataContextValue } from '../../../../context/has_data_context'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { ObservabilityPluginSetupDeps } from '../../../../plugin'; + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/observability/overview/', + search: '', + }), + useHistory: jest.fn(), +})); describe('APMSection', () => { + beforeAll(() => { + jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({ + hasData: { + apm: { + status: fetcherHook.FETCH_STATUS.SUCCESS, + hasData: true, + }, + }, + } as HasDataContextValue); + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: ({ + uiSettings: { get: jest.fn() }, + http: { basePath: { prepend: jest.fn() } }, + } as unknown) as CoreStart, + appMountParameters: {} as AppMountParameters, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: '2020-10-08T06:00:00.000Z', + to: '2020-10-08T07:00:00.000Z', + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + }); it('renders with transaction series and stats', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, queryAllByTestId } = render( - <APMSection - absoluteTime={{ - start: moment('2020-06-29T11:38:23.747Z').valueOf(), - end: moment('2020-06-29T12:08:23.748Z').valueOf(), - }} - relativeTime={{ start: 'now-15m', end: 'now' }} - bucketSize="60s" - /> - ); + const { getByText, queryAllByTestId } = render(<APMSection bucketSize="60s" />); expect(getByText('APM')).toBeInTheDocument(); expect(getByText('View in app')).toBeInTheDocument(); @@ -40,16 +74,7 @@ describe('APMSection', () => { status: fetcherHook.FETCH_STATUS.LOADING, refetch: jest.fn(), }); - const { getByText, queryAllByText, getByTestId } = render( - <APMSection - absoluteTime={{ - start: moment('2020-06-29T11:38:23.747Z').valueOf(), - end: moment('2020-06-29T12:08:23.748Z').valueOf(), - }} - relativeTime={{ start: 'now-15m', end: 'now' }} - bucketSize="60s" - /> - ); + const { getByText, queryAllByText, getByTestId } = render(<APMSection bucketSize="60s" />); expect(getByText('APM')).toBeInTheDocument(); expect(getByTestId('loading')).toBeInTheDocument(); 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 b635c2c68b926..91d20d3478960 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 @@ -12,17 +12,17 @@ import moment from 'moment'; import React, { useContext } from 'react'; import { useHistory } from 'react-router-dom'; import { ThemeContext } from 'styled-components'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; import { ChartContainer } from '../../chart_container'; import { StyledStat } from '../../styled_stat'; import { onBrushEnd } from '../helper'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } @@ -30,25 +30,36 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } -export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); + const chartTheme = useChartTheme(); const history = useHistory(); + const { forceUpdate, hasData } = useHasData(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('apm')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('apm')?.fetchData({ + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, relativeStart, relativeEnd, forceUpdate] + ); + + if (!hasData.apm?.hasData) { + return null; + } const { appLink, stats, series } = data || {}; - const min = moment.utc(absoluteTime.start).valueOf(); - const max = moment.utc(absoluteTime.end).valueOf(); + const min = moment.utc(absoluteStart).valueOf(); + const max = moment.utc(absoluteEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); @@ -93,7 +104,7 @@ export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) { <ChartContainer isInitialLoad={isLoading && !data}> <Settings onBrushEnd={({ x }) => onBrushEnd({ x, history })} - theme={useChartTheme()} + theme={chartTheme} showLegend={false} xDomain={{ min, max }} /> diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index 343611294bc45..f60cab86453d1 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -5,19 +5,19 @@ */ import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind, EuiSpacer, EuiTitle } from '@elastic/eui'; import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import moment from 'moment'; import React, { Fragment } from 'react'; import { useHistory } from 'react-router-dom'; -import { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer } from '@elastic/eui'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { LogsFetchDataResponse } from '../../../../typings'; import { formatStatValue } from '../../../../utils/format_stat_value'; import { ChartContainer } from '../../chart_container'; @@ -25,8 +25,6 @@ import { StyledStat } from '../../styled_stat'; import { onBrushEnd } from '../helper'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } @@ -45,22 +43,33 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) { return colorsPerItem; } -export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function LogsSection({ bucketSize }: Props) { const history = useHistory(); + const chartTheme = useChartTheme(); + const { forceUpdate, hasData } = useHasData(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('infra_logs')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('infra_logs')?.fetchData({ + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, relativeStart, relativeEnd, forceUpdate] + ); + + if (!hasData.infra_logs?.hasData) { + return null; + } - const min = moment.utc(absoluteTime.start).valueOf(); - const max = moment.utc(absoluteTime.end).valueOf(); + const min = moment.utc(absoluteStart).valueOf(); + const max = moment.utc(absoluteEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); @@ -115,7 +124,7 @@ export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) { <ChartContainer isInitialLoad={isLoading && !data}> <Settings onBrushEnd={({ x }) => onBrushEnd({ x, history })} - theme={useChartTheme()} + theme={chartTheme} showLegend legendPosition={Position.Right} xDomain={{ min, max }} diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx index 8bce8205902fa..f7fe3f5694a4a 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -13,13 +13,13 @@ import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { Series } from '../../../../typings'; import { ChartContainer } from '../../chart_container'; import { StyledStat } from '../../styled_stat'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } @@ -46,19 +46,29 @@ const StyledProgress = styled.div<{ color?: string }>` } `; -export function MetricsSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function MetricsSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); + const { forceUpdate, hasData } = useHasData(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('infra_metrics')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('infra_metrics')?.fetchData({ + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, relativeStart, relativeEnd, forceUpdate] + ); + + if (!hasData.infra_metrics?.hasData) { + return null; + } const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 879d745ff2b64..b0710a5c695a7 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -24,34 +24,45 @@ import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { Series } from '../../../../typings'; import { ChartContainer } from '../../chart_container'; import { StyledStat } from '../../styled_stat'; import { onBrushEnd } from '../helper'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } -export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function UptimeSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); + const chartTheme = useChartTheme(); const history = useHistory(); + const { forceUpdate, hasData } = useHasData(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('uptime')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('uptime')?.fetchData({ + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, relativeStart, relativeEnd, forceUpdate] + ); + + if (!hasData.uptime?.hasData) { + return null; + } - const min = moment.utc(absoluteTime.start).valueOf(); - const max = moment.utc(absoluteTime.end).valueOf(); + const min = moment.utc(absoluteStart).valueOf(); + const max = moment.utc(absoluteEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); @@ -112,7 +123,7 @@ export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) <ChartContainer isInitialLoad={isLoading && !data}> <Settings onBrushEnd={({ x }) => onBrushEnd({ x, history })} - theme={useChartTheme()} + theme={chartTheme} showLegend={false} legendPosition={Position.Right} xDomain={{ min, max }} diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index ef1820eaaeb3e..be6df55166387 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -3,31 +3,63 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; -import moment from 'moment'; +import { HasDataContextValue } from '../../../../context/has_data_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; +import * as hasDataHook from '../../../../hooks/use_has_data'; +import * as pluginContext from '../../../../hooks/use_plugin_context'; +import { ObservabilityPluginSetupDeps } from '../../../../plugin'; import { render } from '../../../../utils/test_helper'; import { UXSection } from './'; import { response } from './mock_data/ux.mock'; +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/observability/overview/', + search: '', + }), +})); + describe('UXSection', () => { + beforeAll(() => { + jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({ + hasData: { + ux: { + status: fetcherHook.FETCH_STATUS.SUCCESS, + hasData: { hasData: true, serviceName: 'elastic-co-frontend' }, + }, + }, + } as HasDataContextValue); + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: ({ + uiSettings: { get: jest.fn() }, + http: { basePath: { prepend: jest.fn() } }, + } as unknown) as CoreStart, + appMountParameters: {} as AppMountParameters, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: '2020-10-08T06:00:00.000Z', + to: '2020-10-08T07:00:00.000Z', + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + }); it('renders with core web vitals', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, getAllByText } = render( - <UXSection - absoluteTime={{ - start: moment('2020-06-29T11:38:23.747Z').valueOf(), - end: moment('2020-06-29T12:08:23.748Z').valueOf(), - }} - relativeTime={{ start: 'now-15m', end: 'now' }} - bucketSize="60s" - serviceName="elastic-co-frontend" - /> - ); + const { getByText, getAllByText } = render(<UXSection bucketSize="60s" />); expect(getByText('User Experience')).toBeInTheDocument(); expect(getByText('View in app')).toBeInTheDocument(); @@ -59,17 +91,7 @@ describe('UXSection', () => { status: fetcherHook.FETCH_STATUS.LOADING, refetch: jest.fn(), }); - const { getByText, queryAllByText, getAllByText } = render( - <UXSection - absoluteTime={{ - start: moment('2020-06-29T11:38:23.747Z').valueOf(), - end: moment('2020-06-29T12:08:23.748Z').valueOf(), - }} - relativeTime={{ start: 'now-15m', end: 'now' }} - bucketSize="60s" - serviceName="elastic-co-frontend" - /> - ); + const { getByText, queryAllByText, getAllByText } = render(<UXSection bucketSize="60s" />); expect(getByText('User Experience')).toBeInTheDocument(); expect(getAllByText('--')).toHaveLength(3); @@ -82,17 +104,7 @@ describe('UXSection', () => { status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, queryAllByText, getAllByText } = render( - <UXSection - absoluteTime={{ - start: moment('2020-06-29T11:38:23.747Z').valueOf(), - end: moment('2020-06-29T12:08:23.748Z').valueOf(), - }} - relativeTime={{ start: 'now-15m', end: 'now' }} - bucketSize="60s" - serviceName="elastic-co-frontend" - /> - ); + const { getByText, queryAllByText, getAllByText } = render(<UXSection bucketSize="60s" />); expect(getByText('User Experience')).toBeInTheDocument(); expect(getAllByText('No data is available.')).toHaveLength(3); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 0c40ce0bf7a2e..43f1072d06fc2 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -9,28 +9,40 @@ import React from 'react'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; +import { UXHasDataResponse } from '../../../../typings'; import { CoreVitals } from '../../../shared/core_web_vitals'; interface Props { - serviceName: string; bucketSize: string; - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; } -export function UXSection({ serviceName, bucketSize, absoluteTime, relativeTime }: Props) { - const { start, end } = absoluteTime; - - const { data, status } = useFetcher(() => { - if (start && end) { - return getDataHandler('ux')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - serviceName, - bucketSize, - }); - } - }, [start, end, relativeTime, serviceName, bucketSize]); +export function UXSection({ bucketSize }: Props) { + const { forceUpdate, hasData } = useHasData(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); + const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {}; + const serviceName = uxHasDataResponse.serviceName as string; + + const { data, status } = useFetcher( + () => { + if (serviceName && bucketSize) { + return getDataHandler('ux')?.fetchData({ + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, + serviceName, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, relativeStart, relativeEnd, forceUpdate, serviceName] + ); + + if (!uxHasDataResponse?.hasData) { + return null; + } const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx index 55746ff6576a9..4819a0760d88a 100644 --- a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx @@ -12,11 +12,11 @@ import { EuiHorizontalRule, EuiListGroupItem, EuiPopoverProps, + EuiListGroupItemProps, } from '@elastic/eui'; - import React, { HTMLAttributes, ReactNode } from 'react'; -import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; import styled from 'styled-components'; +import { EuiListGroupProps } from '@elastic/eui'; type Props = EuiPopoverProps & HTMLAttributes<HTMLDivElement>; @@ -42,9 +42,9 @@ export function SectionSubtitle({ children }: { children?: ReactNode }) { ); } -export function SectionLinks({ children }: { children?: ReactNode }) { +export function SectionLinks({ children, ...props }: { children?: ReactNode } & EuiListGroupProps) { return ( - <EuiListGroup flush={true} bordered={false}> + <EuiListGroup {...props} flush={true} bordered={false}> {children} </EuiListGroup> ); diff --git a/x-pack/plugins/observability/public/components/shared/date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/date_picker/index.tsx index 747ec8a441c42..32c6c6054f775 100644 --- a/x-pack/plugins/observability/public/components/shared/date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/date_picker/index.tsx @@ -7,6 +7,7 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { useHasData } from '../../../hooks/use_has_data'; import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings'; import { usePluginContext } from '../../../hooks/use_plugin_context'; import { fromQuery, toQuery } from '../../../utils/url'; @@ -36,6 +37,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval const location = useLocation(); const history = useHistory(); const { plugins } = usePluginContext(); + const { onRefreshTimeRange } = useHasData(); useEffect(() => { plugins.data.query.timefilter.timefilter.setTime({ @@ -81,6 +83,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval function onTimeChange({ start, end }: { start: string; end: string }) { updateUrl({ rangeFrom: start, rangeTo: end }); + onRefreshTimeRange(); } return ( diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx new file mode 100644 index 0000000000000..3369765c68bd1 --- /dev/null +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -0,0 +1,467 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +// import { act, getByText } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { CoreStart } from 'kibana/public'; +import React from 'react'; +import { registerDataHandler, unregisterDataHandler } from '../data_handler'; +import { useHasData } from '../hooks/use_has_data'; +import * as routeParams from '../hooks/use_route_params'; +import * as timeRange from '../hooks/use_time_range'; +import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data'; +import { HasDataContextProvider } from './has_data_context'; +import * as pluginContext from '../hooks/use_plugin_context'; +import { PluginContextValue } from './plugin_context'; + +const relativeStart = '2020-10-08T06:00:00.000Z'; +const relativeEnd = '2020-10-08T07:00:00.000Z'; + +function wrapper({ children }: { children: React.ReactElement }) { + return <HasDataContextProvider>{children}</HasDataContextProvider>; +} + +function unregisterAll() { + unregisterDataHandler({ appName: 'apm' }); + unregisterDataHandler({ appName: 'infra_logs' }); + unregisterDataHandler({ appName: 'infra_metrics' }); + unregisterDataHandler({ appName: 'uptime' }); + unregisterDataHandler({ appName: 'ux' }); +} + +function registerApps<T extends ObservabilityFetchDataPlugins>( + apps: Array<{ appName: T; hasData: HasData<T> }> +) { + apps.forEach(({ appName, hasData }) => { + registerDataHandler({ + appName, + fetchData: () => ({} as any), + hasData, + }); + }); +} + +describe('HasDataContextProvider', () => { + beforeAll(() => { + jest.spyOn(routeParams, 'useRouteParams').mockImplementation(() => ({ + query: { + from: relativeStart, + to: relativeEnd, + }, + path: {}, + })); + jest.spyOn(timeRange, 'useTimeRange').mockImplementation(() => ({ + relativeStart, + relativeEnd, + absoluteStart: new Date(relativeStart).valueOf(), + absoluteEnd: new Date(relativeEnd).valueOf(), + })); + jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({ + core: ({ http: { get: jest.fn() } } as unknown) as CoreStart, + } as PluginContextValue); + }); + + describe('when no plugin has registered', () => { + it('hasAnyData returns false and all apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toMatchObject({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + describe('when plugins have registered', () => { + describe('all apps return false', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => false }, + { appName: 'infra_logs', hasData: async () => false }, + { appName: 'infra_metrics', hasData: async () => false }, + { appName: 'uptime', hasData: async () => false }, + { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns false and all apps return false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: false, status: 'success' }, + uptime: { hasData: false, status: 'success' }, + infra_logs: { hasData: false, status: 'success' }, + infra_metrics: { hasData: false, status: 'success' }, + ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + + describe('at least one app returns true', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => true }, + { appName: 'infra_logs', hasData: async () => false }, + { appName: 'infra_metrics', hasData: async () => false }, + { appName: 'uptime', hasData: async () => false }, + { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true apm returns true and all other apps return false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: false, status: 'success' }, + infra_logs: { hasData: false, status: 'success' }, + infra_metrics: { hasData: false, status: 'success' }, + ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + + describe('all apps return true', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => true }, + { appName: 'infra_logs', hasData: async () => true }, + { appName: 'infra_metrics', hasData: async () => true }, + { appName: 'uptime', hasData: async () => true }, + { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true and all apps return true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: true, status: 'success' }, + infra_logs: { hasData: true, status: 'success' }, + infra_metrics: { hasData: true, status: 'success' }, + ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + + describe('only apm is registered', () => { + describe('when apm returns true', () => { + beforeAll(() => { + registerApps([{ appName: 'apm', hasData: async () => true }]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true, apm returns true and all other apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { + wrapper, + }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + + describe('when apm returns false', () => { + beforeAll(() => { + registerApps([{ appName: 'apm', hasData: async () => false }]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns false, apm returns false and all other apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { + wrapper, + }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: false, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + }); + + describe('when an app throws an error while fetching', () => { + beforeAll(() => { + registerApps([ + { + appName: 'apm', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { appName: 'infra_logs', hasData: async () => true }, + { appName: 'infra_metrics', hasData: async () => true }, + { appName: 'uptime', hasData: async () => true }, + { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true, apm is undefined and all other apps return true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'failure' }, + uptime: { hasData: true, status: 'success' }, + infra_logs: { hasData: true, status: 'success' }, + infra_metrics: { hasData: true, status: 'success' }, + ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + + describe('when all apps throw an error while fetching', () => { + beforeAll(() => { + registerApps([ + { + appName: 'apm', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'infra_logs', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'infra_metrics', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'uptime', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'ux', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns false and all apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'failure' }, + uptime: { hasData: undefined, status: 'failure' }, + infra_logs: { hasData: undefined, status: 'failure' }, + infra_metrics: { hasData: undefined, status: 'failure' }, + ux: { hasData: undefined, status: 'failure' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + }); + + describe('with alerts', () => { + beforeAll(() => { + jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({ + core: ({ + http: { + get: async () => { + return { + data: [ + { id: 2, consumer: 'apm' }, + { id: 3, consumer: 'uptime' }, + ], + }; + }, + }, + } as unknown) as CoreStart, + } as PluginContextValue); + }); + + it('returns all alerts available', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { + hasData: [ + { id: 2, consumer: 'apm' }, + { id: 3, consumer: 'uptime' }, + ], + status: 'success', + }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx new file mode 100644 index 0000000000000..79d58056af73c --- /dev/null +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniqueId } from 'lodash'; +import React, { createContext, useEffect, useState } from 'react'; +import { Alert } from '../../../alerts/common'; +import { getDataHandler } from '../data_handler'; +import { FETCH_STATUS } from '../hooks/use_fetcher'; +import { usePluginContext } from '../hooks/use_plugin_context'; +import { useTimeRange } from '../hooks/use_time_range'; +import { getObservabilityAlerts } from '../services/get_observability_alerts'; +import { ObservabilityFetchDataPlugins, UXHasDataResponse } from '../typings/fetch_overview_data'; + +type DataContextApps = ObservabilityFetchDataPlugins | 'alert'; + +export type HasDataMap = Record< + DataContextApps, + { status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse | Alert[] } +>; + +export interface HasDataContextValue { + hasData: Partial<HasDataMap>; + hasAnyData: boolean; + isAllRequestsComplete: boolean; + onRefreshTimeRange: () => void; + forceUpdate: string; +} + +export const HasDataContext = createContext({} as HasDataContextValue); + +const apps: DataContextApps[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics', 'ux', 'alert']; + +export function HasDataContextProvider({ children }: { children: React.ReactNode }) { + const { core } = usePluginContext(); + const [forceUpdate, setForceUpdate] = useState(''); + const { absoluteStart, absoluteEnd } = useTimeRange(); + + const [hasData, setHasData] = useState<HasDataContextValue['hasData']>({}); + + useEffect( + () => { + apps.forEach(async (app) => { + try { + if (app !== 'alert') { + const params = + app === 'ux' + ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } + : undefined; + + const result = await getDataHandler(app)?.hasData(params); + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: result, + status: FETCH_STATUS.SUCCESS, + }, + })); + } + } catch (e) { + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + useEffect(() => { + async function fetchAlerts() { + try { + const alerts = await getObservabilityAlerts({ core }); + setHasData((prevState) => ({ + ...prevState, + alert: { + hasData: alerts, + status: FETCH_STATUS.SUCCESS, + }, + })); + } catch (e) { + setHasData((prevState) => ({ + ...prevState, + alert: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + } + + fetchAlerts(); + }, [forceUpdate, core]); + + const isAllRequestsComplete = apps.every((app) => { + const appStatus = hasData[app]?.status; + return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; + }); + + const hasAnyData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some( + (app) => hasData[app]?.hasData === true + ); + + return ( + <HasDataContext.Provider + value={{ + hasData, + hasAnyData, + isAllRequestsComplete, + forceUpdate, + onRefreshTimeRange: () => { + setForceUpdate(uniqueId()); + }, + }} + children={children} + /> + ); +} diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index 8fdfc2bc622ca..f555f11be2251 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -3,20 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - registerDataHandler, - getDataHandler, - unregisterDataHandler, - fetchHasData, -} from './data_handler'; +import { registerDataHandler, getDataHandler } from './data_handler'; import moment from 'moment'; -import { - ApmFetchDataResponse, - LogsFetchDataResponse, - MetricsFetchDataResponse, - UptimeFetchDataResponse, - UxFetchDataResponse, -} from './typings'; const params = { absoluteTime: { @@ -447,203 +435,4 @@ describe('registerDataHandler', () => { expect(hasData).toBeTruthy(); }); }); - describe('fetchHasData', () => { - it('returns false when an exception happens', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: false, - uptime: false, - infra_logs: false, - infra_metrics: false, - ux: false, - }); - }); - it('returns true when has data and false when an exception happens', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: true, - uptime: false, - infra_logs: true, - infra_metrics: false, - ux: false, - }); - }); - it('returns true when has data', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => ({ - hasData: true, - serviceName: 'elastic-co', - }), - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: true, - uptime: true, - infra_logs: true, - infra_metrics: true, - ux: { - hasData: true, - serviceName: 'elastic-co', - }, - }); - }); - it('returns false when has no data', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => false, - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: false, - uptime: false, - infra_logs: false, - infra_metrics: false, - ux: false, - }); - }); - it('returns false when has data was not registered', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: false, - uptime: false, - infra_logs: false, - infra_metrics: false, - ux: false, - }); - }); - }); }); diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 91043a3da0dab..7ee7db7ede17d 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - DataHandler, - HasDataResponse, - ObservabilityFetchDataPlugins, -} from './typings/fetch_overview_data'; +import { DataHandler, ObservabilityFetchDataPlugins } from './typings/fetch_overview_data'; const dataHandlers: Partial<Record<ObservabilityFetchDataPlugins, DataHandler>> = {}; @@ -34,40 +30,3 @@ export function getDataHandler<T extends ObservabilityFetchDataPlugins>(appName: return dataHandler as DataHandler<T>; } } - -export async function fetchHasData(absoluteTime: { - start: number; - end: number; -}): Promise<Record<ObservabilityFetchDataPlugins, HasDataResponse>> { - const apps: ObservabilityFetchDataPlugins[] = [ - 'apm', - 'uptime', - 'infra_logs', - 'infra_metrics', - 'ux', - ]; - - const promises = apps.map( - async (app) => - getDataHandler(app)?.hasData(app === 'ux' ? { absoluteTime } : undefined) || false - ); - - const results = await Promise.allSettled(promises); - - const [apm, uptime, logs, metrics, ux] = results.map((result) => { - if (result.status === 'fulfilled') { - return result.value; - } - - console.error('Error while fetching has data', result.reason); - return false; - }); - - return { - apm, - uptime, - ux, - infra_logs: logs, - infra_metrics: metrics, - }; -} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/scoring_rt.ts b/x-pack/plugins/observability/public/hooks/use_has_data.ts similarity index 51% rename from x-pack/plugins/apm/server/lib/transaction_groups/correlations/scoring_rt.ts rename to x-pack/plugins/observability/public/hooks/use_has_data.ts index cb94b6251eb07..9c66fa8861420 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/scoring_rt.ts +++ b/x-pack/plugins/observability/public/hooks/use_has_data.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; +import { useContext } from 'react'; +import { HasDataContext } from '../context/has_data_context'; -export const scoringRt = t.union([ - t.literal('jlh'), - t.literal('chi_square'), - t.literal('gnd'), - t.literal('percentage'), -]); - -export type SignificantTermsScoring = t.TypeOf<typeof scoringRt>; +export function useHasData() { + return useContext(HasDataContext); +} diff --git a/x-pack/plugins/observability/public/hooks/use_route_params.tsx b/x-pack/plugins/observability/public/hooks/use_route_params.tsx index 1b32933eec3e6..9774d9bed4244 100644 --- a/x-pack/plugins/observability/public/hooks/use_route_params.tsx +++ b/x-pack/plugins/observability/public/hooks/use_route_params.tsx @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { useLocation, useParams } from 'react-router-dom'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { Params } from '../routes'; +import { Params, RouteParams, routes } from '../routes'; function getQueryParams(location: ReturnType<typeof useLocation>) { const urlSearchParms = new URLSearchParams(location.search); @@ -23,14 +23,15 @@ function getQueryParams(location: ReturnType<typeof useLocation>) { * It removes any aditional item which is not declared in the type. * @param params */ -export function useRouteParams(params: Params) { +export function useRouteParams<T extends keyof typeof routes>(pathName: T): RouteParams<T> { const location = useLocation(); const pathParams = useParams(); const queryParams = getQueryParams(location); + const { query, path } = routes[pathName].params as Params; const rts = { - queryRt: params.query ? t.exact(params.query) : t.strict({}), - pathRt: params.path ? t.exact(params.path) : t.strict({}), + queryRt: query ? t.exact(query) : t.strict({}), + pathRt: path ? t.exact(path) : t.strict({}), }; const queryResult = rts.queryRt.decode(queryParams); @@ -43,8 +44,8 @@ export function useRouteParams(params: Params) { console.error(PathReporter.report(pathResult)[0]); } - return { + return ({ query: isLeft(queryResult) ? {} : queryResult.right, path: isLeft(pathResult) ? {} : pathResult.right, - }; + } as unknown) as RouteParams<T>; } diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts new file mode 100644 index 0000000000000..c89d52f904a96 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useTimeRange } from './use_time_range'; +import * as pluginContext from './use_plugin_context'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { ObservabilityPluginSetupDeps } from '../plugin'; +import * as kibanaUISettings from './use_kibana_ui_settings'; + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/observability/overview/', + search: '', + }), +})); + +describe('useTimeRange', () => { + beforeAll(() => { + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: {} as CoreStart, + appMountParameters: {} as AppMountParameters, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: '2020-10-08T06:00:00.000Z', + to: '2020-10-08T07:00:00.000Z', + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({ + from: '2020-10-08T05:00:00.000Z', + to: '2020-10-08T06:00:00.000Z', + })); + }); + + describe('when range from and to are not provided', () => { + describe('when data plugin has time set', () => { + it('returns ranges and absolute times from data plugin', () => { + const relativeStart = '2020-10-08T06:00:00.000Z'; + const relativeEnd = '2020-10-08T07:00:00.000Z'; + const timeRange = useTimeRange(); + expect(timeRange).toEqual({ + relativeStart, + relativeEnd, + absoluteStart: new Date(relativeStart).valueOf(), + absoluteEnd: new Date(relativeEnd).valueOf(), + }); + }); + }); + describe("when data plugin doesn't have time set", () => { + beforeAll(() => { + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: {} as CoreStart, + appMountParameters: {} as AppMountParameters, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: undefined, + to: undefined, + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + }); + it('returns ranges and absolute times from kibana default settings', () => { + const relativeStart = '2020-10-08T05:00:00.000Z'; + const relativeEnd = '2020-10-08T06:00:00.000Z'; + const timeRange = useTimeRange(); + expect(timeRange).toEqual({ + relativeStart, + relativeEnd, + absoluteStart: new Date(relativeStart).valueOf(), + absoluteEnd: new Date(relativeEnd).valueOf(), + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.ts b/x-pack/plugins/observability/public/hooks/use_time_range.ts new file mode 100644 index 0000000000000..e8bed12aaa9bd --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_time_range.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 { parse } from 'query-string'; +import { useLocation } from 'react-router-dom'; +import { TimePickerTime } from '../components/shared/date_picker'; +import { getAbsoluteTime } from '../utils/date'; +import { UI_SETTINGS, useKibanaUISettings } from './use_kibana_ui_settings'; +import { usePluginContext } from './use_plugin_context'; + +const getParsedParams = (search: string) => { + return parse(search.slice(1), { sort: false }); +}; + +export function useTimeRange() { + const { plugins } = usePluginContext(); + + const timePickerTimeDefaults = useKibanaUISettings<TimePickerTime>( + UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS + ); + + const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime(); + + const { rangeFrom, rangeTo } = getParsedParams(useLocation().search); + + const relativeStart = (rangeFrom ?? + timePickerSharedState.from ?? + timePickerTimeDefaults.from) as string; + const relativeEnd = (rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to) as string; + + return { + relativeStart, + relativeEnd, + absoluteStart: getAbsoluteTime(relativeStart)!, + absoluteEnd: getAbsoluteTime(relativeEnd, { roundUp: true })!, + }; +} diff --git a/x-pack/plugins/observability/public/pages/home/index.test.tsx b/x-pack/plugins/observability/public/pages/home/index.test.tsx new file mode 100644 index 0000000000000..2c06b7035f515 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/home/index.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { HasDataContextValue } from '../../context/has_data_context'; +import * as hasData from '../../hooks/use_has_data'; +import { render } from '../../utils/test_helper'; +import { HomePage } from './'; + +const mockHistoryPush = jest.fn(); +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +describe('Home page', () => { + beforeAll(() => { + jest.restoreAllMocks(); + }); + + it('renders loading component while requests are not returned', () => { + jest + .spyOn(hasData, 'useHasData') + .mockImplementation( + () => + ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false } as HasDataContextValue) + ); + const { getByText } = render(<HomePage />); + expect(getByText('Loading Observability')).toBeInTheDocument(); + }); + it('renders landing page', () => { + jest + .spyOn(hasData, 'useHasData') + .mockImplementation( + () => + ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true } as HasDataContextValue) + ); + render(<HomePage />); + expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/landing' }); + }); + it('renders overview page', () => { + jest + .spyOn(hasData, 'useHasData') + .mockImplementation( + () => + ({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false } as HasDataContextValue) + ); + render(<HomePage />); + expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/overview' }); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index 77b812dddd327..a2a7cad1d5620 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -5,33 +5,20 @@ */ import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { fetchHasData } from '../../data_handler'; -import { useFetcher } from '../../hooks/use_fetcher'; -import { useQueryParams } from '../../hooks/use_query_params'; +import { useHasData } from '../../hooks/use_has_data'; import { LoadingObservability } from '../overview/loading_observability'; export function HomePage() { const history = useHistory(); - - const { absStart, absEnd } = useQueryParams(); - - const { data = {} } = useFetcher( - () => fetchHasData({ start: absStart, end: absEnd }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const values = Object.values(data); - const hasSomeData = values.length ? values.some((hasData) => hasData) : null; + const { hasAnyData, isAllRequestsComplete } = useHasData(); useEffect(() => { - if (hasSomeData === true) { + if (hasAnyData === true) { history.push({ pathname: '/overview' }); - } - if (hasSomeData === false) { + } else if (hasAnyData === false && isAllRequestsComplete === true) { history.push({ pathname: '/landing' }); } - }, [hasSomeData, history]); + }, [hasAnyData, isAllRequestsComplete, history]); return <LoadingObservability />; } diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 7377a1ca0ea52..b5302d5f17f5c 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -30,8 +30,8 @@ const EuiCardWithoutPadding = styled(EuiCard)` `; export function LandingPage() { - useTrackPageview({ app: 'observability', path: 'landing' }); - useTrackPageview({ app: 'observability', path: 'landing', delay: 15000 }); + useTrackPageview({ app: 'observability-overview', path: 'landing' }); + useTrackPageview({ app: 'observability-overview', path: 'landing', delay: 15000 }); const { core } = usePluginContext(); const theme = useContext(ThemeContext); diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index 2d3142d4e5804..f0c56eb7137e2 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -4,76 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { APMSection } from '../../components/app/section/apm'; import { LogsSection } from '../../components/app/section/logs'; import { MetricsSection } from '../../components/app/section/metrics'; -import { APMSection } from '../../components/app/section/apm'; import { UptimeSection } from '../../components/app/section/uptime'; import { UXSection } from '../../components/app/section/ux'; -import { - HasDataResponse, - ObservabilityFetchDataPlugins, - UXHasDataResponse, -} from '../../typings/fetch_overview_data'; +import { HasDataMap } from '../../context/has_data_context'; interface Props { bucketSize: string; - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; - hasData: Record<ObservabilityFetchDataPlugins, HasDataResponse>; + hasData?: Partial<HasDataMap>; } -export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime }: Props) { +export function DataSections({ bucketSize }: Props) { return ( <EuiFlexItem grow={false}> <EuiFlexGroup direction="column"> - {hasData?.infra_logs && ( - <EuiFlexItem grow={false}> - <LogsSection - bucketSize={bucketSize} - absoluteTime={absoluteTime} - relativeTime={relativeTime} - /> - </EuiFlexItem> - )} - {hasData?.infra_metrics && ( - <EuiFlexItem grow={false}> - <MetricsSection - bucketSize={bucketSize} - absoluteTime={absoluteTime} - relativeTime={relativeTime} - /> - </EuiFlexItem> - )} - {hasData?.apm && ( - <EuiFlexItem grow={false}> - <APMSection - bucketSize={bucketSize} - absoluteTime={absoluteTime} - relativeTime={relativeTime} - /> - </EuiFlexItem> - )} - {hasData?.uptime && ( - <EuiFlexItem grow={false}> - <UptimeSection - bucketSize={bucketSize} - absoluteTime={absoluteTime} - relativeTime={relativeTime} - /> - </EuiFlexItem> - )} - {(hasData.ux as UXHasDataResponse).hasData && ( - <EuiFlexItem grow={false}> - <UXSection - serviceName={(hasData.ux as UXHasDataResponse).serviceName as string} - bucketSize={bucketSize} - absoluteTime={absoluteTime} - relativeTime={relativeTime} - /> - </EuiFlexItem> - )} + <EuiFlexItem grow={false}> + <LogsSection bucketSize={bucketSize} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <MetricsSection bucketSize={bucketSize} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <APMSection bucketSize={bucketSize} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <UptimeSection bucketSize={bucketSize} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <UXSection bucketSize={bucketSize} /> + </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> ); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index ec00a5b416034..87a836b2cb32c 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -3,27 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useContext } from 'react'; import { ThemeContext } from 'styled-components'; -import { useTrackPageview, UXHasDataResponse } from '../..'; -import { EmptySection } from '../../components/app/empty_section'; +import { useTrackPageview } from '../..'; +import { Alert } from '../../../../alerts/common'; +import { EmptySections } from '../../components/app/empty_sections'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { NewsFeed } from '../../components/app/news_feed'; import { Resources } from '../../components/app/resources'; import { AlertsSection } from '../../components/app/section/alerts'; -import { DatePicker, TimePickerTime } from '../../components/shared/date_picker'; -import { fetchHasData } from '../../data_handler'; -import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; -import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings'; +import { DatePicker } from '../../components/shared/date_picker'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useTimeRange } from '../../hooks/use_time_range'; import { RouteParams } from '../../routes'; import { getNewsFeed } from '../../services/get_news_feed'; -import { getObservabilityAlerts } from '../../services/get_observability_alerts'; -import { getAbsoluteTime } from '../../utils/date'; import { getBucketSize } from '../../utils/get_bucket_size'; import { DataSections } from './data_sections'; -import { getEmptySections } from './empty_section'; import { LoadingObservability } from './loading_observability'; interface Props { @@ -37,47 +35,26 @@ function calculateBucketSize({ start, end }: { start?: number; end?: number }) { } export function OverviewPage({ routeParams }: Props) { - const { core, plugins } = usePluginContext(); - - // read time from state and update the url - const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime(); - - const timePickerDefaults = useKibanaUISettings<TimePickerTime>( - UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS - ); - - const relativeTime = { - start: routeParams.query.rangeFrom || timePickerSharedState.from || timePickerDefaults.from, - end: routeParams.query.rangeTo || timePickerSharedState.to || timePickerDefaults.to, - }; - - const absoluteTime = { - start: getAbsoluteTime(relativeTime.start) as number, - end: getAbsoluteTime(relativeTime.end, { roundUp: true }) as number, - }; + useTrackPageview({ app: 'observability-overview', path: 'overview' }); + useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); + const { core } = usePluginContext(); + const theme = useContext(ThemeContext); - useTrackPageview({ app: 'observability', path: 'overview' }); - useTrackPageview({ app: 'observability', path: 'overview', delay: 15000 }); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const { data: alerts = [], status: alertStatus } = useFetcher(() => { - return getObservabilityAlerts({ core }); - }, [core]); + const relativeTime = { start: relativeStart, end: relativeEnd }; + const absoluteTime = { start: absoluteStart, end: absoluteEnd }; const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); - const theme = useContext(ThemeContext); + const { hasData, hasAnyData } = useHasData(); - const result = useFetcher( - () => fetchHasData(absoluteTime), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const hasData = result.data; - - if (!hasData) { + if (hasAnyData === undefined) { return <LoadingObservability />; } + const alerts = (hasData.alert?.hasData as Alert[]) || []; + const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; const bucketSize = calculateBucketSize({ @@ -85,18 +62,6 @@ export function OverviewPage({ routeParams }: Props) { end: absoluteTime.end, }); - const appEmptySections = getEmptySections({ core }).filter(({ id }) => { - if (id === 'alert') { - return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; - } else if (id === 'ux') { - return !(hasData[id] as UXHasDataResponse).hasData; - } - return !hasData[id]; - }); - - // Hides the data section when all 'hasData' is false or undefined - const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData); - return ( <WithHeaderLayout headerColor={theme.eui.euiColorEmptyShade} @@ -113,42 +78,9 @@ export function OverviewPage({ routeParams }: Props) { <EuiFlexGroup> <EuiFlexItem grow={6}> {/* Data sections */} - {showDataSections && ( - <DataSections - hasData={hasData} - absoluteTime={absoluteTime} - relativeTime={relativeTime} - bucketSize={bucketSize?.intervalString!} - /> - )} - - {/* Empty sections */} - {!!appEmptySections.length && ( - <EuiFlexItem> - <EuiSpacer size="s" /> - <EuiFlexGrid - columns={ - // when more than 2 empty sections are available show them on 2 columns, otherwise 1 - appEmptySections.length > 2 ? 2 : 1 - } - gutterSize="s" - > - {appEmptySections.map((app) => { - return ( - <EuiFlexItem - key={app.id} - style={{ - border: `1px dashed ${theme.eui.euiBorderColor}`, - borderRadius: '4px', - }} - > - <EmptySection section={app} /> - </EuiFlexItem> - ); - })} - </EuiFlexGrid> - </EuiFlexItem> - )} + {hasAnyData && <DataSections bucketSize={bucketSize?.intervalString!} />} + + <EmptySections /> </EuiFlexItem> {/* Alert section */} diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 8713bb1229273..a28e34e7d4dcb 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -10,6 +10,7 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; +import { HasDataContextProvider } from '../../context/has_data_context'; import { PluginContext } from '../../context/plugin_context'; import { registerDataHandler, unregisterDataHandler } from '../../data_handler'; import { ObservabilityPluginSetupDeps } from '../../plugin'; @@ -52,7 +53,9 @@ const withCore = makeDecorator({ } as unknown) as ObservabilityPluginSetupDeps, }} > - <EuiThemeProvider>{storyFn(context)}</EuiThemeProvider> + <EuiThemeProvider> + <HasDataContextProvider>{storyFn(context)}</HasDataContextProvider> + </EuiThemeProvider> </PluginContext.Provider> </MemoryRouter> ); diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts index 64f5f4aab1c2b..e3f8f877656bd 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AppMountContext } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { getObservabilityAlerts } from './get_observability_alerts'; const basePath = { prepend: (path: string) => path }; @@ -27,10 +27,9 @@ describe('getObservabilityAlerts', () => { }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; - const alerts = await getObservabilityAlerts({ core }); - expect(alerts).toEqual([]); + expect(getObservabilityAlerts({ core })).rejects.toThrow('Boom'); }); it('Returns empty array when api return undefined', async () => { @@ -43,7 +42,7 @@ describe('getObservabilityAlerts', () => { }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([]); @@ -55,32 +54,17 @@ describe('getObservabilityAlerts', () => { get: async () => { return { data: [ - { - id: 1, - consumer: 'siem', - }, - { - id: 2, - consumer: 'kibana', - }, - { - id: 3, - consumer: 'index', - }, - { - id: 4, - consumer: 'foo', - }, - { - id: 5, - consumer: 'bar', - }, + { id: 1, consumer: 'siem' }, + { id: 2, consumer: 'kibana' }, + { id: 3, consumer: 'index' }, + { id: 4, consumer: 'foo' }, + { id: 5, consumer: 'bar' }, ], }; }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([]); }); @@ -91,36 +75,18 @@ describe('getObservabilityAlerts', () => { get: async () => { return { data: [ - { - id: 1, - consumer: 'siem', - }, - { - id: 2, - consumer: 'apm', - }, - { - id: 3, - consumer: 'uptime', - }, - { - id: 4, - consumer: 'logs', - }, - { - id: 5, - consumer: 'metrics', - }, - { - id: 6, - consumer: 'alerts', - }, + { id: 1, consumer: 'siem' }, + { id: 2, consumer: 'apm' }, + { id: 3, consumer: 'uptime' }, + { id: 4, consumer: 'logs' }, + { id: 5, consumer: 'metrics' }, + { id: 6, consumer: 'alerts' }, ], }; }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([ diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts index cff6726e47df9..b1f8f0fb1bddc 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AppMountContext } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { Alert } from '../../../alerts/common'; const allowedConsumers = ['apm', 'uptime', 'logs', 'metrics', 'alerts']; -export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) { +export async function getObservabilityAlerts({ core }: { core: CoreStart }) { try { - const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', { - query: { - page: 1, - per_page: 20, - }, - }); + const { data = [] }: { data: Alert[] } = + (await core.http.get('/api/alerts/_find', { + query: { + page: 1, + per_page: 20, + }, + })) || {}; return data.filter(({ consumer }) => allowedConsumers.includes(consumer)); } catch (e) { console.error('Error while fetching alerts', e); - return []; + throw e; } } diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index a64e6fc55b85a..4cac1d586f295 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -37,24 +37,24 @@ export interface UXHasDataResponse { serviceName: string | number | undefined; } -export type HasDataResponse = UXHasDataResponse | boolean; - export type FetchData<T extends FetchDataResponse = FetchDataResponse> = ( fetchDataParams: FetchDataParams ) => Promise<T>; -export type HasData = (params?: HasDataParams) => Promise<HasDataResponse>; +export type HasData<T extends ObservabilityFetchDataPlugins> = ( + params?: HasDataParams +) => Promise<ObservabilityHasDataResponse[T]>; export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, - 'observability' | 'stack_monitoring' + 'observability-overview' | 'stack_monitoring' >; export interface DataHandler< T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins > { fetchData: FetchData<ObservabilityFetchDataResponse[T]>; - hasData: HasData; + hasData: HasData<T>; } export interface FetchDataResponse { @@ -113,3 +113,11 @@ export interface ObservabilityFetchDataResponse { uptime: UptimeFetchDataResponse; ux: UxFetchDataResponse; } + +export interface ObservabilityHasDataResponse { + apm: boolean; + infra_metrics: boolean; + infra_logs: boolean; + uptime: boolean; + ux: UXHasDataResponse; +} diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts index c86eb924a051e..8093d6077148e 100644 --- a/x-pack/plugins/observability/typings/common.ts +++ b/x-pack/plugins/observability/typings/common.ts @@ -9,7 +9,7 @@ export type ObservabilityApp = | 'infra_logs' | 'apm' | 'uptime' - | 'observability' + | 'observability-overview' | 'stack_monitoring' | 'ux'; diff --git a/x-pack/plugins/saved_objects_tagging/README.md b/x-pack/plugins/saved_objects_tagging/README.md index 5e4281a8c4e7d..0da16746f6494 100644 --- a/x-pack/plugins/saved_objects_tagging/README.md +++ b/x-pack/plugins/saved_objects_tagging/README.md @@ -1,3 +1,53 @@ # SavedObjectsTagging -Add tagging capability to saved objects \ No newline at end of file +Add tagging capability to saved objects + +## Integrating tagging on a new object type + +In addition to use the UI api to plug the tagging feature in your application, there is a couple +things that needs to be done on the server: + +### Add read-access to the `tag` SO type to your feature's capabilities + +In order to be able to fetch the tags assigned to an object, the user must have read permission +for the `tag` saved object type. Which is why all features relying on SO tagging must update +their capabilities. + +```typescript +features.registerKibanaFeature({ + id: 'myFeature', + // ... + privileges: { + all: { + // ... + savedObject: { + all: ['some-type'], + read: ['tag'], // <-- HERE + }, + }, + read: { + // ... + savedObject: { + all: [], + read: ['some-type', 'tag'], // <-- AND HERE + }, + }, + }, +}); +``` + +### Update the SOT telemetry collector schema to add the new type + +The schema is located here: `x-pack/plugins/saved_objects_tagging/server/usage/schema.ts`. You +just need to add the name of the SO type you are adding. + +```ts +export const tagUsageCollectorSchema: MakeSchemaFrom<TaggingUsageData> = { + // ... + types: { + dashboard: perTypeSchema, + visualization: perTypeSchema, + // <-- add your type here + }, +}; +``` diff --git a/x-pack/plugins/saved_objects_tagging/kibana.json b/x-pack/plugins/saved_objects_tagging/kibana.json index 89c5e7a134339..134e48a671f28 100644 --- a/x-pack/plugins/saved_objects_tagging/kibana.json +++ b/x-pack/plugins/saved_objects_tagging/kibana.json @@ -6,5 +6,6 @@ "ui": true, "configPath": ["xpack", "saved_object_tagging"], "requiredPlugins": ["features", "management", "savedObjectsTaggingOss"], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact"], + "optionalPlugins": ["usageCollection"] } diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts index 1223b1ec20389..f0c3285667817 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts @@ -8,3 +8,8 @@ export const registerRoutesMock = jest.fn(); jest.doMock('./routes', () => ({ registerRoutes: registerRoutesMock, })); + +export const createTagUsageCollectorMock = jest.fn(); +jest.doMock('./usage', () => ({ + createTagUsageCollector: createTagUsageCollectorMock, +})); diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts index 1a3e4071f5e09..0730b29cde4a8 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts @@ -4,20 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerRoutesMock } from './plugin.test.mocks'; +import { registerRoutesMock, createTagUsageCollectorMock } from './plugin.test.mocks'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; import { SavedObjectTaggingPlugin } from './plugin'; import { savedObjectsTaggingFeature } from './features'; describe('SavedObjectTaggingPlugin', () => { let plugin: SavedObjectTaggingPlugin; let featuresPluginSetup: ReturnType<typeof featuresPluginMock.createSetup>; + let usageCollectionSetup: ReturnType<typeof usageCollectionPluginMock.createSetupContract>; beforeEach(() => { plugin = new SavedObjectTaggingPlugin(coreMock.createPluginInitializerContext()); featuresPluginSetup = featuresPluginMock.createSetup(); + usageCollectionSetup = usageCollectionPluginMock.createSetupContract(); + // `usageCollection` 'mocked' implementation use the real `CollectorSet` implementation + // that throws when registering things that are not collectors. + // We just want to assert that it was called here, so jest.fn is fine. + usageCollectionSetup.registerCollector = jest.fn(); + }); + + afterEach(() => { + registerRoutesMock.mockReset(); + createTagUsageCollectorMock.mockReset(); }); describe('#setup', () => { @@ -43,5 +55,18 @@ describe('SavedObjectTaggingPlugin', () => { savedObjectsTaggingFeature ); }); + + it('registers the usage collector if `usageCollection` is present', async () => { + const tagUsageCollector = Symbol('saved_objects_tagging'); + createTagUsageCollectorMock.mockReturnValue(tagUsageCollector); + + await plugin.setup(coreMock.createSetup(), { + features: featuresPluginSetup, + usageCollection: usageCollectionSetup, + }); + + expect(usageCollectionSetup.registerCollector).toHaveBeenCalledTimes(1); + expect(usageCollectionSetup.registerCollector).toHaveBeenCalledWith(tagUsageCollector); + }); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.ts index 8347fb1f8ef20..6eb8080793d0e 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.ts @@ -4,22 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart, PluginInitializerContext, Plugin } from 'src/core/server'; +import { Observable } from 'rxjs'; +import { + CoreSetup, + CoreStart, + PluginInitializerContext, + Plugin, + SharedGlobalConfig, +} from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { savedObjectsTaggingFeature } from './features'; import { tagType } from './saved_objects'; import { ITagsRequestHandlerContext } from './types'; -import { registerRoutes } from './routes'; import { TagsRequestHandlerContext } from './request_handler_context'; +import { registerRoutes } from './routes'; +import { createTagUsageCollector } from './usage'; interface SetupDeps { features: FeaturesPluginSetup; + usageCollection?: UsageCollectionSetup; } export class SavedObjectTaggingPlugin implements Plugin<{}, {}, SetupDeps, {}> { - constructor(context: PluginInitializerContext) {} + private readonly legacyConfig$: Observable<SharedGlobalConfig>; - public setup({ savedObjects, http }: CoreSetup, { features }: SetupDeps) { + constructor(context: PluginInitializerContext) { + this.legacyConfig$ = context.config.legacy.globalConfig$; + } + + public setup({ savedObjects, http }: CoreSetup, { features, usageCollection }: SetupDeps) { savedObjects.registerType(tagType); const router = http.createRouter(); @@ -34,6 +48,15 @@ export class SavedObjectTaggingPlugin implements Plugin<{}, {}, SetupDeps, {}> { features.registerKibanaFeature(savedObjectsTaggingFeature); + if (usageCollection) { + usageCollection.registerCollector( + createTagUsageCollector({ + usageCollection, + legacyConfig$: this.legacyConfig$, + }) + ); + } + return {}; } diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts b/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts new file mode 100644 index 0000000000000..692088e66003e --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchClient } from 'src/core/server'; +import { TaggingUsageData, ByTypeTaggingUsageData } from './types'; + +/** + * Manual type reflection of the `tagDataAggregations` resulting payload + */ +interface AggregatedTagUsageResponseBody { + aggregations: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + nested_ref: { + tag_references: { + doc_count: number; + tag_id: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + }; + }>; + }; + }; +} + +export const fetchTagUsageData = async ({ + esClient, + kibanaIndex, +}: { + esClient: ElasticsearchClient; + kibanaIndex: string; +}): Promise<TaggingUsageData> => { + const { body } = await esClient.search<AggregatedTagUsageResponseBody>({ + index: [kibanaIndex], + ignore_unavailable: true, + filter_path: 'aggregations', + body: { + size: 0, + query: { + bool: { + must: [hasTagReferenceClause], + }, + }, + aggs: tagDataAggregations, + }, + }); + + const byTypeUsages: Record<string, ByTypeTaggingUsageData> = {}; + const allUsedTags = new Set<string>(); + let totalTaggedObjects = 0; + + const typeBuckets = body.aggregations.by_type.buckets; + typeBuckets.forEach((bucket) => { + const type = bucket.key; + const taggedDocCount = bucket.doc_count; + const usedTagIds = bucket.nested_ref.tag_references.tag_id.buckets.map( + (tagBucket) => tagBucket.key + ); + + totalTaggedObjects += taggedDocCount; + usedTagIds.forEach((tagId) => allUsedTags.add(tagId)); + + byTypeUsages[type] = { + taggedObjects: taggedDocCount, + usedTags: usedTagIds.length, + }; + }); + + return { + usedTags: allUsedTags.size, + taggedObjects: totalTaggedObjects, + types: byTypeUsages, + }; +}; + +const hasTagReferenceClause = { + nested: { + path: 'references', + query: { + bool: { + must: [ + { + term: { + 'references.type': 'tag', + }, + }, + ], + }, + }, + }, +}; + +const tagDataAggregations = { + by_type: { + terms: { + field: 'type', + }, + aggs: { + nested_ref: { + nested: { + path: 'references', + }, + aggs: { + tag_references: { + filter: { + term: { + 'references.type': 'tag', + }, + }, + aggs: { + tag_id: { + terms: { + field: 'references.id', + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/apm/scripts/shared/stamp-logger.ts b/x-pack/plugins/saved_objects_tagging/server/usage/index.ts similarity index 65% rename from x-pack/plugins/apm/scripts/shared/stamp-logger.ts rename to x-pack/plugins/saved_objects_tagging/server/usage/index.ts index 65d24bbae7008..023295ab19aef 100644 --- a/x-pack/plugins/apm/scripts/shared/stamp-logger.ts +++ b/x-pack/plugins/saved_objects_tagging/server/usage/index.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import consoleStamp from 'console-stamp'; - -export function stampLogger() { - consoleStamp(console, { pattern: '[HH:MM:ss.l]' }); -} +export { createTagUsageCollector } from './tag_usage_collector'; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts b/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts new file mode 100644 index 0000000000000..8132c60daf964 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/schema.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 { MakeSchemaFrom } from '../../../../../src/plugins/usage_collection/server'; +import { TaggingUsageData, ByTypeTaggingUsageData } from './types'; + +const perTypeSchema: MakeSchemaFrom<ByTypeTaggingUsageData> = { + usedTags: { type: 'integer' }, + taggedObjects: { type: 'integer' }, +}; + +export const tagUsageCollectorSchema: MakeSchemaFrom<TaggingUsageData> = { + usedTags: { type: 'integer' }, + taggedObjects: { type: 'integer' }, + + types: { + dashboard: perTypeSchema, + visualization: perTypeSchema, + map: perTypeSchema, + }, +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts b/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts new file mode 100644 index 0000000000000..a38dc46193332 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { SharedGlobalConfig } from 'src/core/server'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server'; +import { TaggingUsageData } from './types'; +import { fetchTagUsageData } from './fetch_tag_usage_data'; +import { tagUsageCollectorSchema } from './schema'; + +export const createTagUsageCollector = ({ + usageCollection, + legacyConfig$, +}: { + usageCollection: UsageCollectionSetup; + legacyConfig$: Observable<SharedGlobalConfig>; +}) => { + return usageCollection.makeUsageCollector<TaggingUsageData>({ + type: 'saved_objects_tagging', + isReady: () => true, + schema: tagUsageCollectorSchema, + fetch: async ({ esClient }) => { + const { kibana } = await legacyConfig$.pipe(take(1)).toPromise(); + return fetchTagUsageData({ esClient, kibanaIndex: kibana.index }); + }, + }); +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/types.ts b/x-pack/plugins/saved_objects_tagging/server/usage/types.ts new file mode 100644 index 0000000000000..3f6ebb752de13 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * @internal + */ +export interface TaggingUsageData { + usedTags: number; + taggedObjects: number; + types: Record<string, ByTypeTaggingUsageData>; +} + +/** + * @internal + */ +export interface ByTypeTaggingUsageData { + usedTags: number; + taggedObjects: number; +} diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index a0d63c0a9dd6f..07e6ab6c72cb9 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -17,3 +17,5 @@ export const UNKNOWN_SPACE = '?'; export const GLOBAL_RESOURCE = '*'; export const APPLICATION_PREFIX = 'kibana-'; export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; + +export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint'; diff --git a/x-pack/plugins/security/common/login_state.ts b/x-pack/plugins/security/common/login_state.ts index fd2b1cb8d1cf7..77edd1a4ea8dd 100644 --- a/x-pack/plugins/security/common/login_state.ts +++ b/x-pack/plugins/security/common/login_state.ts @@ -10,6 +10,7 @@ export interface LoginSelectorProvider { type: string; name: string; usesLoginForm: boolean; + showInSelector: boolean; description?: string; hint?: string; icon?: string; diff --git a/x-pack/plugins/security/common/model/authenticated_user.test.ts b/x-pack/plugins/security/common/model/authenticated_user.test.ts index d253fed97f353..6eb428adf2cd5 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.test.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.test.ts @@ -20,6 +20,19 @@ describe('#canUserChangePassword', () => { } as AuthenticatedUser) ).toEqual(true); }); + + it(`returns false for users in the ${realm} realm if used for anonymous access`, () => { + expect( + canUserChangePassword({ + username: 'foo', + authentication_provider: { type: 'anonymous', name: 'does not matter' }, + authentication_realm: { + name: 'the realm name', + type: realm, + }, + } as AuthenticatedUser) + ).toEqual(false); + }); }); it(`returns false for all other realms`, () => { diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts index d5c8d4e474c60..c22c5fc4ef0da 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.ts @@ -42,5 +42,8 @@ export interface AuthenticatedUser extends User { } export function canUserChangePassword(user: AuthenticatedUser) { - return REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(user.authentication_realm.type); + return ( + REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(user.authentication_realm.type) && + user.authentication_provider.type !== 'anonymous' + ); } diff --git a/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap index 8af75633776e8..64d456c3c6b0a 100644 --- a/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap @@ -133,45 +133,6 @@ exports[`LoginPage enabled form state renders as expected 1`] = ` /> `; -exports[`LoginPage enabled form state renders as expected when info message is set 1`] = ` -<LoginForm - http={ - Object { - "addLoadingCountSource": [MockFunction], - "get": [MockFunction], - } - } - infoMessage="Your session has timed out. Please log in again." - loginAssistanceMessage="" - notifications={ - Object { - "toasts": Object { - "add": [MockFunction], - "addDanger": [MockFunction], - "addError": [MockFunction], - "addInfo": [MockFunction], - "addSuccess": [MockFunction], - "addWarning": [MockFunction], - "get$": [MockFunction], - "remove": [MockFunction], - }, - } - } - selector={ - Object { - "enabled": false, - "providers": Array [ - Object { - "name": "basic1", - "type": "basic", - "usesLoginForm": true, - }, - ], - } - } -/> -`; - exports[`LoginPage enabled form state renders as expected when loginAssistanceMessage is set 1`] = ` <LoginForm http={ @@ -180,7 +141,6 @@ exports[`LoginPage enabled form state renders as expected when loginAssistanceMe "get": [MockFunction], } } - infoMessage="Your session has timed out. Please log in again." loginAssistanceMessage="This is an *important* message" notifications={ Object { @@ -219,7 +179,6 @@ exports[`LoginPage enabled form state renders as expected when loginHelp is set "get": [MockFunction], } } - infoMessage="Your session has timed out. Please log in again." loginAssistanceMessage="" loginHelp="**some-help**" notifications={ diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx index e6d170122751e..2b67f20484884 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx @@ -22,23 +22,40 @@ function expectPageMode(wrapper: ReactWrapper, mode: PageMode) { ['loginForm', true], ['loginSelector', false], ['loginHelp', false], + ['autoLoginOverlay', false], ] : mode === PageMode.Selector ? [ ['loginForm', false], ['loginSelector', true], ['loginHelp', false], + ['autoLoginOverlay', false], ] : [ ['loginForm', false], ['loginSelector', false], ['loginHelp', true], + ['autoLoginOverlay', false], ]; for (const [selector, exists] of assertions) { expect(findTestSubject(wrapper, selector).exists()).toBe(exists); } } +function expectAutoLoginOverlay(wrapper: ReactWrapper) { + // Everything should be hidden except for the overlay + for (const selector of [ + 'loginForm', + 'loginSelector', + 'loginHelp', + 'loginHelpLink', + 'loginAssistanceMessage', + ]) { + expect(findTestSubject(wrapper, selector).exists()).toBe(false); + } + expect(findTestSubject(wrapper, 'autoLoginOverlay').exists()).toBe(true); +} + describe('LoginForm', () => { beforeAll(() => { Object.defineProperty(window, 'location', { @@ -57,7 +74,9 @@ describe('LoginForm', () => { loginAssistanceMessage="" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + ], }} /> ) @@ -74,7 +93,7 @@ describe('LoginForm', () => { loginAssistanceMessage="" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }], }} /> ); @@ -94,7 +113,7 @@ describe('LoginForm', () => { loginAssistanceMessage="" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }], }} /> ); @@ -115,7 +134,7 @@ describe('LoginForm', () => { loginAssistanceMessage="" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }], }} /> ); @@ -147,7 +166,7 @@ describe('LoginForm', () => { loginAssistanceMessage="" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }], }} /> ); @@ -180,7 +199,7 @@ describe('LoginForm', () => { loginAssistanceMessage="" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic1', usesLoginForm: true }], + providers: [{ type: 'basic', name: 'basic1', usesLoginForm: true, showInSelector: true }], }} /> ); @@ -222,7 +241,7 @@ describe('LoginForm', () => { loginHelp="**some help**" selector={{ enabled: false, - providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }], }} /> ); @@ -261,14 +280,22 @@ describe('LoginForm', () => { usesLoginForm: true, hint: 'Basic hint', icon: 'logoElastic', + showInSelector: true, + }, + { + type: 'saml', + name: 'saml1', + description: 'Log in w/SAML', + usesLoginForm: false, + showInSelector: true, }, - { type: 'saml', name: 'saml1', description: 'Log in w/SAML', usesLoginForm: false }, { type: 'pki', name: 'pki1', description: 'Log in w/PKI', hint: 'PKI hint', usesLoginForm: false, + showInSelector: true, }, ], }} @@ -309,8 +336,15 @@ describe('LoginForm', () => { description: 'Login w/SAML', hint: 'SAML hint', usesLoginForm: false, + showInSelector: true, + }, + { + type: 'pki', + name: 'pki1', + icon: 'some-icon', + usesLoginForm: false, + showInSelector: true, }, - { type: 'pki', name: 'pki1', icon: 'some-icon', usesLoginForm: false }, ], }} /> @@ -352,9 +386,21 @@ describe('LoginForm', () => { selector={{ enabled: true, providers: [ - { type: 'basic', name: 'basic', usesLoginForm: true }, - { type: 'saml', name: 'saml1', description: 'Login w/SAML', usesLoginForm: false }, - { type: 'pki', name: 'pki1', description: 'Login w/PKI', usesLoginForm: false }, + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + { + type: 'saml', + name: 'saml1', + description: 'Login w/SAML', + usesLoginForm: false, + showInSelector: true, + }, + { + type: 'pki', + name: 'pki1', + description: 'Login w/PKI', + usesLoginForm: false, + showInSelector: true, + }, ], }} /> @@ -397,8 +443,8 @@ describe('LoginForm', () => { selector={{ enabled: true, providers: [ - { type: 'basic', name: 'basic', usesLoginForm: true }, - { type: 'saml', name: 'saml1', usesLoginForm: false }, + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, ], }} /> @@ -445,8 +491,8 @@ describe('LoginForm', () => { selector={{ enabled: true, providers: [ - { type: 'basic', name: 'basic', usesLoginForm: true }, - { type: 'saml', name: 'saml1', usesLoginForm: false }, + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, ], }} /> @@ -488,8 +534,8 @@ describe('LoginForm', () => { selector={{ enabled: true, providers: [ - { type: 'basic', name: 'basic', usesLoginForm: true }, - { type: 'saml', name: 'saml1', usesLoginForm: false }, + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, ], }} /> @@ -517,8 +563,8 @@ describe('LoginForm', () => { selector={{ enabled: true, providers: [ - { type: 'basic', name: 'basic', usesLoginForm: true }, - { type: 'saml', name: 'saml1', usesLoginForm: false }, + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, ], }} /> @@ -554,8 +600,8 @@ describe('LoginForm', () => { selector={{ enabled: true, providers: [ - { type: 'basic', name: 'basic', usesLoginForm: true }, - { type: 'saml', name: 'saml1', usesLoginForm: false }, + { type: 'basic', name: 'basic', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, ], }} /> @@ -591,4 +637,168 @@ describe('LoginForm', () => { expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); }); }); + + describe('auto login', () => { + it('automatically switches to the Login Form mode if provider suggested by the auth provider hint needs it', () => { + const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginHelp={'**Hey this is a login help message**'} + loginAssistanceMessage="Need assistance?" + authProviderHint="basic1" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic1', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Form); + expect(findTestSubject(wrapper, 'loginHelpLink').text()).toEqual('Need help?'); + expect(findTestSubject(wrapper, 'loginAssistanceMessage').text()).toEqual('Need assistance?'); + }); + + it('automatically logs in if provider suggested by the auth provider hint is displayed in the selector', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.post.mockResolvedValue({ + location: 'https://external-idp/login?optional-arg=2#optional-hash', + }); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginHelp={'**Hey this is a login help message**'} + loginAssistanceMessage="Need assistance?" + authProviderHint="saml1" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic1', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, + ], + }} + /> + ); + + expectAutoLoginOverlay(wrapper); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith('/internal/security/login', { + body: JSON.stringify({ providerType: 'saml', providerName: 'saml1', currentURL }), + }); + + expect(window.location.href).toBe('https://external-idp/login?optional-arg=2#optional-hash'); + expect(wrapper.find(EuiCallOut).exists()).toBe(false); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + + it('automatically logs in if provider suggested by the auth provider hint is not displayed in the selector', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.post.mockResolvedValue({ + location: 'https://external-idp/login?optional-arg=2#optional-hash', + }); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginHelp={'**Hey this is a login help message**'} + loginAssistanceMessage="Need assistance?" + authProviderHint="saml1" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic1', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: false }, + ], + }} + /> + ); + + expectAutoLoginOverlay(wrapper); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith('/internal/security/login', { + body: JSON.stringify({ providerType: 'saml', providerName: 'saml1', currentURL }), + }); + + expect(window.location.href).toBe('https://external-idp/login?optional-arg=2#optional-hash'); + expect(wrapper.find(EuiCallOut).exists()).toBe(false); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + + it('switches to the login selector if could not login with provider suggested by the auth provider hint', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const failureReason = new Error('Oh no!'); + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.post.mockRejectedValue(failureReason); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginHelp={'**Hey this is a login help message**'} + loginAssistanceMessage="Need assistance?" + authProviderHint="saml1" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic1', usesLoginForm: true, showInSelector: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false, showInSelector: true }, + ], + }} + /> + ); + + expectAutoLoginOverlay(wrapper); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith('/internal/security/login', { + body: JSON.stringify({ providerType: 'saml', providerName: 'saml1', currentURL }), + }); + + expect(window.location.href).toBe(currentURL); + expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith(failureReason, { + title: 'Could not perform login.', + toastMessage: 'Oh no!', + }); + + expectPageMode(wrapper, PageMode.Selector); + expect(findTestSubject(wrapper, 'loginHelpLink').text()).toEqual('Need help?'); + expect(findTestSubject(wrapper, 'loginAssistanceMessage').text()).toEqual('Need assistance?'); + }); + }); }); diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index 901d43adb659d..e37d0024852d7 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -29,7 +29,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public'; -import { LoginSelector } from '../../../../../common/login_state'; +import type { LoginSelector, LoginSelectorProvider } from '../../../../../common/login_state'; import { LoginValidator } from './validate_login'; interface Props { @@ -39,12 +39,12 @@ interface Props { infoMessage?: string; loginAssistanceMessage: string; loginHelp?: string; + authProviderHint?: string; } interface State { loadingState: - | { type: LoadingStateType.None } - | { type: LoadingStateType.Form } + | { type: LoadingStateType.None | LoadingStateType.Form | LoadingStateType.AutoLogin } | { type: LoadingStateType.Selector; providerName: string }; username: string; password: string; @@ -59,6 +59,7 @@ enum LoadingStateType { None, Form, Selector, + AutoLogin, } enum MessageType { @@ -76,11 +77,26 @@ export enum PageMode { export class LoginForm extends Component<Props, State> { private readonly validator: LoginValidator; + /** + * Optional provider that was suggested by the `auth_provider_hint={providerName}` query string parameter. If provider + * doesn't require Kibana native login form then login process is triggered automatically, otherwise Login Selector + * just switches to the Login Form mode. + */ + private readonly suggestedProvider?: LoginSelectorProvider; + constructor(props: Props) { super(props); this.validator = new LoginValidator({ shouldValidate: false }); - const mode = this.showLoginSelector() ? PageMode.Selector : PageMode.Form; + this.suggestedProvider = this.props.authProviderHint + ? this.props.selector.providers.find(({ name }) => name === this.props.authProviderHint) + : undefined; + + // Switch to the Form mode right away if provider from the hint requires it. + const mode = + this.showLoginSelector() && !this.suggestedProvider?.usesLoginForm + ? PageMode.Selector + : PageMode.Form; this.state = { loadingState: { type: LoadingStateType.None }, @@ -94,7 +110,17 @@ export class LoginForm extends Component<Props, State> { }; } + async componentDidMount() { + if (this.suggestedProvider?.usesLoginForm === false) { + await this.loginWithSelector({ provider: this.suggestedProvider, autoLogin: true }); + } + } + public render() { + if (this.isLoadingState(LoadingStateType.AutoLogin)) { + return this.renderAutoLoginOverlay(); + } + return ( <Fragment> {this.renderLoginAssistanceMessage()} @@ -111,7 +137,7 @@ export class LoginForm extends Component<Props, State> { } return ( - <div className="secLoginAssistanceMessage"> + <div data-test-subj="loginAssistanceMessage" className="secLoginAssistanceMessage"> <EuiHorizontalRule size="half" /> <EuiText size="xs"> <ReactMarkdown>{this.props.loginAssistanceMessage}</ReactMarkdown> @@ -257,9 +283,10 @@ export class LoginForm extends Component<Props, State> { }; private renderSelector = () => { + const providers = this.props.selector.providers.filter((provider) => provider.showInSelector); return ( <EuiPanel data-test-subj="loginSelector" paddingSize="none"> - {this.props.selector.providers.map((provider) => ( + {providers.map((provider) => ( <button key={provider.name} data-test-subj={`loginCard-${provider.type}/${provider.name}`} @@ -267,7 +294,7 @@ export class LoginForm extends Component<Props, State> { onClick={() => provider.usesLoginForm ? this.onPageModeChange(PageMode.Form) - : this.loginWithSelector(provider.type, provider.name) + : this.loginWithSelector({ provider }) } className={`secLoginCard ${ this.isLoadingState(LoadingStateType.Selector, provider.name) @@ -360,6 +387,30 @@ export class LoginForm extends Component<Props, State> { return null; }; + private renderAutoLoginOverlay = () => { + return ( + <EuiFlexGroup + data-test-subj="autoLoginOverlay" + alignItems="center" + justifyContent="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="l" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="m" className="eui-textCenter"> + <FormattedMessage + id="xpack.security.loginPage.autoLoginAuthenticatingLabel" + defaultMessage="Authenticating…" + /> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + ); + }; + private setUsernameInputRef(ref: HTMLInputElement) { if (ref) { ref.focus(); @@ -438,9 +489,17 @@ export class LoginForm extends Component<Props, State> { } }; - private loginWithSelector = async (providerType: string, providerName: string) => { + private loginWithSelector = async ({ + provider: { type: providerType, name: providerName }, + autoLogin, + }: { + provider: LoginSelectorProvider; + autoLogin?: boolean; + }) => { this.setState({ - loadingState: { type: LoadingStateType.Selector, providerName }, + loadingState: autoLogin + ? { type: LoadingStateType.AutoLogin } + : { type: LoadingStateType.Selector, providerName }, message: { type: MessageType.None }, }); @@ -466,7 +525,9 @@ export class LoginForm extends Component<Props, State> { } }; - private isLoadingState(type: LoadingStateType.None | LoadingStateType.Form): boolean; + private isLoadingState( + type: LoadingStateType.None | LoadingStateType.Form | LoadingStateType.AutoLogin + ): boolean; private isLoadingState(type: LoadingStateType.Selector, providerName: string): boolean; private isLoadingState(type: LoadingStateType, providerName?: string) { const { loadingState } = this.state; @@ -482,7 +543,9 @@ export class LoginForm extends Component<Props, State> { private showLoginSelector() { return ( this.props.selector.enabled && - this.props.selector.providers.some((provider) => !provider.usesLoginForm) + this.props.selector.providers.some( + (provider) => !provider.usesLoginForm && provider.showInSelector + ) ); } } diff --git a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx index 467b2a7ff9906..7110c8e130ac1 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { act } from '@testing-library/react'; import { nextTick } from '@kbn/test/jest'; +import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants'; import { LoginState } from '../../../common/login_state'; import { LoginPage } from './login_page'; import { coreMock } from '../../../../../../src/core/public/mocks'; @@ -37,14 +38,12 @@ describe('LoginPage', () => { httpMock.addLoadingCountSource.mockReset(); }; - beforeAll(() => { + beforeEach(() => { Object.defineProperty(window, 'location', { value: { href: 'http://some-host/bar', protocol: 'http' }, writable: true, }); - }); - beforeEach(() => { resetHttpMock(); }); @@ -206,10 +205,10 @@ describe('LoginPage', () => { expect(wrapper.find(LoginForm)).toMatchSnapshot(); }); - it('renders as expected when info message is set', async () => { + it('properly passes query string parameters to the form', async () => { const coreStartMock = coreMock.createStart(); httpMock.get.mockResolvedValue(createLoginState()); - window.location.href = 'http://some-host/bar?msg=SESSION_EXPIRED'; + window.location.href = `http://some-host/bar?msg=SESSION_EXPIRED&${AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER}=basic1`; const wrapper = shallow( <LoginPage @@ -226,7 +225,9 @@ describe('LoginPage', () => { resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot }); - expect(wrapper.find(LoginForm)).toMatchSnapshot(); + const { authProviderHint, infoMessage } = wrapper.find(LoginForm).props(); + expect(authProviderHint).toBe('basic1'); + expect(infoMessage).toBe('Your session has timed out. Please log in again.'); }); it('renders as expected when loginAssistanceMessage is set', async () => { diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index be152b21e2701..0646962684284 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -15,6 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle } from '@elasti import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public'; +import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants'; import { LoginState } from '../../../common/login_state'; import { LoginForm, DisabledLoginForm } from './components'; @@ -212,14 +213,16 @@ export class LoginPage extends Component<Props, State> { ); } + const query = parse(window.location.href, true).query; return ( <LoginForm http={this.props.http} notifications={this.props.notifications} selector={selector} - infoMessage={infoMessageMap.get(parse(window.location.href, true).query.msg?.toString())} + infoMessage={infoMessageMap.get(query.msg?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} loginHelp={loginHelp} + authProviderHint={query[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]?.toString()} /> ); }; 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 e65310ba399ea..5479bc36d1ed5 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 @@ -83,18 +83,18 @@ describe('roleMappingsManagementApp', () => { }); it('mount() works for the `edit role mapping` page', async () => { - const roleMappingName = 'someRoleMappingName'; + const roleMappingName = 'role@mapping'; const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${roleMappingName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Role Mappings' }, - { href: `/edit/${roleMappingName}`, text: roleMappingName }, + { href: `/edit/${encodeURIComponent(roleMappingName)}`, text: roleMappingName }, ]); expect(container).toMatchInlineSnapshot(` <div> - Role Mapping Edit Page: {"name":"someRoleMappingName","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/someRoleMappingName","search":"","hash":""}}} + Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}} </div> `); 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 bca3a070e64f9..ce4ded5a9acbc 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 @@ -12,6 +12,7 @@ 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'; interface CreateParams { getStartServices: StartServicesAccessor<PluginStartDependencies>; @@ -70,10 +71,14 @@ export const roleMappingsManagementApp = Object.freeze({ const EditRoleMappingsPageWithBreadcrumbs = () => { const { name } = useParams<{ name?: string }>(); + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const decodedName = name ? tryDecodeURIComponent(name) : undefined; + setBreadcrumbs([ ...roleMappingsBreadcrumbs, name - ? { text: name, href: `/edit/${encodeURIComponent(name)}` } + ? { text: decodedName, href: `/edit/${encodeURIComponent(name)}` } : { text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { defaultMessage: 'Create', @@ -83,7 +88,7 @@ export const roleMappingsManagementApp = Object.freeze({ return ( <EditRoleMappingPage - name={name} + name={decodedName} roleMappingsAPI={roleMappingsAPIClient} rolesAPIClient={new RolesAPIClient(http)} notifications={notifications} 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 c45528399db99..8bcf58428c08d 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 @@ -97,18 +97,18 @@ describe('rolesManagementApp', () => { }); it('mount() works for the `edit role` page', async () => { - const roleName = 'someRoleName'; + const roleName = 'role@name'; const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${roleName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Roles' }, - { href: `/edit/${roleName}`, text: roleName }, + { href: `/edit/${encodeURIComponent(roleName)}`, text: roleName }, ]); expect(container).toMatchInlineSnapshot(` <div> - Role Edit Page: {"action":"edit","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} </div> `); 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 88aeb1d232fc7..d5b3b4998a09d 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 @@ -13,6 +13,7 @@ import { RegisterManagementAppArgs } from '../../../../../../src/plugins/managem import { SecurityLicense } from '../../../common/licensing'; import { PluginStartDependencies } from '../../plugin'; import { DocumentationLinksService } from './documentation_links'; +import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { fatalErrors: FatalErrorsSetup; @@ -68,10 +69,14 @@ export const rolesManagementApp = Object.freeze({ const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { const { roleName } = useParams<{ roleName?: string }>(); + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const decodedRoleName = roleName ? tryDecodeURIComponent(roleName) : undefined; + setBreadcrumbs([ ...rolesBreadcrumbs, action === 'edit' && roleName - ? { text: roleName, href: `/edit/${encodeURIComponent(roleName)}` } + ? { text: decodedRoleName, href: `/edit/${encodeURIComponent(roleName)}` } : { text: i18n.translate('xpack.security.roles.createBreadcrumb', { defaultMessage: 'Create', @@ -82,7 +87,7 @@ export const rolesManagementApp = Object.freeze({ return ( <EditRolePage action={action} - roleName={roleName} + roleName={decodedRoleName} rolesAPIClient={rolesAPIClient} userAPIClient={new UserAPIClient(http)} indicesAPIClient={new IndicesAPIClient(http)} diff --git a/x-pack/plugins/security/public/management/uri_utils.test.ts b/x-pack/plugins/security/public/management/uri_utils.test.ts new file mode 100644 index 0000000000000..029228d911c05 --- /dev/null +++ b/x-pack/plugins/security/public/management/uri_utils.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { tryDecodeURIComponent } from './url_utils'; + +describe('tryDecodeURIComponent', () => { + it('properly decodes a URI Component', () => { + expect( + tryDecodeURIComponent('sample%26piece%3Dof%20text%40gmail.com%2520') + ).toMatchInlineSnapshot(`"sample&piece=of text@gmail.com%20"`); + }); + + it('returns the original string undecoded if it is malformed', () => { + expect(tryDecodeURIComponent('sample&piece=of%text@gmail.com%20')).toMatchInlineSnapshot( + `"sample&piece=of%text@gmail.com%20"` + ); + }); +}); diff --git a/x-pack/plugins/security/public/management/url_utils.ts b/x-pack/plugins/security/public/management/url_utils.ts new file mode 100644 index 0000000000000..590863e30d5ec --- /dev/null +++ b/x-pack/plugins/security/public/management/url_utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const tryDecodeURIComponent = (uriComponent: string) => { + try { + return decodeURIComponent(uriComponent); + } catch { + return uriComponent; + } +}; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx index 06bd2eff6aa1e..c9e448d90d925 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -86,18 +86,18 @@ describe('usersManagementApp', () => { }); it('mount() works for the `edit user` page', async () => { - const userName = 'someUserName'; + const userName = 'foo@bar.com'; const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${userName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Users' }, - { href: `/edit/${userName}`, text: userName }, + { href: `/edit/${encodeURIComponent(userName)}`, text: userName }, ]); expect(container).toMatchInlineSnapshot(` <div> - User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/someUserName","search":"","hash":""}}} + User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"foo@bar.com","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/foo@bar.com","search":"","hash":""}}} </div> `); @@ -106,18 +106,23 @@ describe('usersManagementApp', () => { expect(container).toMatchInlineSnapshot(`<div />`); }); - it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { - const username = 'some 安全性 user'; - - const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { - href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', - text: username, - }, - ]); + const usernames = ['foo@bar.com', 'foo&bar.com', 'some 安全性 user']; + usernames.forEach((username) => { + it( + 'mount() properly encodes user name in `edit user` page link in breadcrumbs for user ' + + username, + async () => { + const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `/`, text: 'Users' }, + { + href: `/edit/${encodeURIComponent(username)}`, + text: username, + }, + ]); + } + ); }); }); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 82c55d67b9026..2f16f85d5fcae 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -12,6 +12,7 @@ import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; +import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { authc: AuthenticationServiceSetup; @@ -66,10 +67,14 @@ export const usersManagementApp = Object.freeze({ const EditUserPageWithBreadcrumbs = () => { const { username } = useParams<{ username?: string }>(); + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const decodedUsername = username ? tryDecodeURIComponent(username) : undefined; + setBreadcrumbs([ ...usersBreadcrumbs, username - ? { text: username, href: `/edit/${encodeURIComponent(username)}` } + ? { text: decodedUsername, href: `/edit/${encodeURIComponent(username)}` } : { text: i18n.translate('xpack.security.users.createBreadcrumb', { defaultMessage: 'Create', @@ -83,7 +88,7 @@ export const usersManagementApp = Object.freeze({ userAPIClient={userAPIClient} rolesAPIClient={new RolesAPIClient(http)} notifications={notifications} - username={username} + username={decodedUsername} history={history} /> ); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index 4a2b86447b7f7..66b8002788dcb 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -8,14 +8,16 @@ import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { shallowWithIntl, nextTick, mountWithIntl } from '@kbn/test/jest'; import { SecurityNavControl } from './nav_control_component'; -import { AuthenticatedUser } from '../../common/model'; +import type { AuthenticatedUser } from '../../common/model'; import { EuiPopover, EuiHeaderSectionItemButton } from '@elastic/eui'; import { findTestSubject } from '@kbn/test/jest'; +import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; + describe('SecurityNavControl', () => { it(`renders a loading spinner when the user promise hasn't resolved yet.`, async () => { const props = { - user: new Promise(() => {}) as Promise<AuthenticatedUser>, + user: new Promise<AuthenticatedUser>(() => mockAuthenticatedUser()), editProfileUrl: '', logoutUrl: '', userMenuLinks$: new BehaviorSubject([]), @@ -41,7 +43,7 @@ describe('SecurityNavControl', () => { it(`renders an avatar after the user promise resolves.`, async () => { const props = { - user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>, + user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })), editProfileUrl: '', logoutUrl: '', userMenuLinks$: new BehaviorSubject([]), @@ -70,7 +72,7 @@ describe('SecurityNavControl', () => { it(`doesn't render the popover when the user hasn't been loaded yet`, async () => { const props = { - user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>, + user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })), editProfileUrl: '', logoutUrl: '', userMenuLinks$: new BehaviorSubject([]), @@ -92,7 +94,7 @@ describe('SecurityNavControl', () => { it('renders a popover when the avatar is clicked.', async () => { const props = { - user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>, + user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })), editProfileUrl: '', logoutUrl: '', userMenuLinks$: new BehaviorSubject([]), @@ -115,7 +117,7 @@ describe('SecurityNavControl', () => { it('renders a popover with additional user menu links registered by other plugins', async () => { const props = { - user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>, + user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })), editProfileUrl: '', logoutUrl: '', userMenuLinks$: new BehaviorSubject([ @@ -145,4 +147,37 @@ describe('SecurityNavControl', () => { expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(1); expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); }); + + it('properly renders a popover for anonymous user.', async () => { + const props = { + user: Promise.resolve( + mockAuthenticatedUser({ + authentication_provider: { type: 'anonymous', name: 'does no matter' }, + }) + ), + editProfileUrl: '', + logoutUrl: '', + userMenuLinks$: new BehaviorSubject([ + { label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 }, + { label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 }, + { label: 'link3', href: 'path-to-link-3', iconType: 'empty', order: 3 }, + ]), + }; + + const wrapper = mountWithIntl(<SecurityNavControl {...props} />); + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(0); + expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(0); + expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(0); + + wrapper.find(EuiHeaderSectionItemButton).simulate('click'); + + expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(1); + expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(0); + expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); + + expect(findTestSubject(wrapper, 'logoutLink').text()).toBe('Log in'); + }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index c22308fa8a43e..e846539025452 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -118,33 +118,23 @@ export class SecurityNavControl extends Component<Props, State> { </EuiHeaderSectionItemButton> ); - const profileMenuItem = { - name: ( - <FormattedMessage - id="xpack.security.navControlComponent.editProfileLinkText" - defaultMessage="Profile" - /> - ), - icon: <EuiIcon type="user" size="m" />, - href: editProfileUrl, - 'data-test-subj': 'profileLink', - }; - - const logoutMenuItem = { - name: ( - <FormattedMessage - id="xpack.security.navControlComponent.logoutLinkText" - defaultMessage="Log out" - /> - ), - icon: <EuiIcon type="exit" size="m" />, - href: logoutUrl, - 'data-test-subj': 'logoutLink', - }; - + const isAnonymousUser = authenticatedUser?.authentication_provider.type === 'anonymous'; const items: EuiContextMenuPanelItemDescriptor[] = []; - items.push(profileMenuItem); + if (!isAnonymousUser) { + const profileMenuItem = { + name: ( + <FormattedMessage + id="xpack.security.navControlComponent.editProfileLinkText" + defaultMessage="Profile" + /> + ), + icon: <EuiIcon type="user" size="m" />, + href: editProfileUrl, + 'data-test-subj': 'profileLink', + }; + items.push(profileMenuItem); + } if (userMenuLinks.length) { const userMenuLinkMenuItems = userMenuLinks @@ -162,6 +152,22 @@ export class SecurityNavControl extends Component<Props, State> { }); } + const logoutMenuItem = { + name: isAnonymousUser ? ( + <FormattedMessage + id="xpack.security.navControlComponent.loginLinkText" + defaultMessage="Log in" + /> + ) : ( + <FormattedMessage + id="xpack.security.navControlComponent.logoutLinkText" + defaultMessage="Log out" + /> + ), + icon: <EuiIcon type="exit" size="m" />, + href: logoutUrl, + 'data-test-subj': 'logoutLink', + }; items.push(logoutMenuItem); const panels = [ diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index eef45598d1761..718415e485725 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -10,6 +10,7 @@ import { ILegacyClusterClient, IBasePath, } from '../../../../../src/core/server'; +import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../common/constants'; import type { SecurityLicense } from '../../common/licensing'; import type { AuthenticatedUser } from '../../common/model'; import type { AuthenticationProvider } from '../../common/types'; @@ -20,6 +21,7 @@ import type { SecurityFeatureUsageServiceStart } from '../feature_usage'; import type { SessionValue, Session } from '../session_management'; import { + AnonymousAuthenticationProvider, AuthenticationProviderOptions, AuthenticationProviderSpecificOptions, BaseAuthenticationProvider, @@ -86,6 +88,7 @@ const providerMap = new Map< [TokenAuthenticationProvider.type, TokenAuthenticationProvider], [OIDCAuthenticationProvider.type, OIDCAuthenticationProvider], [PKIAuthenticationProvider.type, PKIAuthenticationProvider], + [AnonymousAuthenticationProvider.type, AnonymousAuthenticationProvider], ]); /** @@ -328,19 +331,26 @@ export class Authenticator { assertRequest(request); const existingSessionValue = await this.getSessionValue(request); + const suggestedProviderName = + existingSessionValue?.provider.name ?? + request.url.searchParams.get(AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER); if (this.shouldRedirectToLoginSelector(request, existingSessionValue)) { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` - )}` + )}${ + suggestedProviderName && !existingSessionValue + ? `&${AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER}=${encodeURIComponent( + suggestedProviderName + )}` + : '' + }` ); } - for (const [providerName, provider] of this.providerIterator( - existingSessionValue?.provider.name - )) { + for (const [providerName, provider] of this.providerIterator(suggestedProviderName)) { // Check if current session has been set by this provider. const ownsSession = existingSessionValue?.provider.name === providerName && diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts new file mode 100644 index 0000000000000..c296cb9c8e94d --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { mockAuthenticationProviderOptions } from './base.mock'; + +import { ILegacyClusterClient, ScopeableRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { + BasicHTTPAuthorizationHeaderCredentials, + HTTPAuthorizationHeader, +} from '../http_authentication'; +import { AnonymousAuthenticationProvider } from './anonymous'; + +function expectAuthenticateCall( + mockClusterClient: jest.Mocked<ILegacyClusterClient>, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + +describe('AnonymousAuthenticationProvider', () => { + const user = mockAuthenticatedUser({ + authentication_provider: { type: 'anonymous', name: 'anonymous1' }, + }); + + for (const useBasicCredentials of [true, false]) { + describe(`with ${useBasicCredentials ? '`Basic`' : '`ApiKey`'} credentials`, () => { + let provider: AnonymousAuthenticationProvider; + let mockOptions: ReturnType<typeof mockAuthenticationProviderOptions>; + let authorization: string; + beforeEach(() => { + mockOptions = mockAuthenticationProviderOptions({ name: 'anonymous1' }); + + provider = useBasicCredentials + ? new AnonymousAuthenticationProvider(mockOptions, { + credentials: { username: 'user', password: 'pass' }, + }) + : new AnonymousAuthenticationProvider(mockOptions, { + credentials: { apiKey: 'some-apiKey' }, + }); + authorization = useBasicCredentials + ? new HTTPAuthorizationHeader( + 'Basic', + new BasicHTTPAuthorizationHeaderCredentials('user', 'pass').toString() + ).toString() + : new HTTPAuthorizationHeader('ApiKey', 'some-apiKey').toString(); + }); + + describe('`login` method', () => { + it('succeeds if credentials are valid, and creates session and authHeaders', async () => { + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect( + provider.login(httpServerMock.createKibanaRequest({ headers: {} })) + ).resolves.toEqual( + AuthenticationResult.succeeded(user, { + authHeaders: { authorization }, + state: {}, + }) + ); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + }); + + it('fails if user cannot be retrieved during login attempt', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const authenticationError = new Error('Some error'); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.login(request)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(request.headers).not.toHaveProperty('authorization'); + }); + }); + + describe('`authenticate` method', () => { + it('does not create session for AJAX requests.', async () => { + // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and + // avoid triggering of redirect logic. + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + }); + + it('does not create session for request that do not require authentication.', async () => { + await expect( + provider.authenticate(httpServerMock.createKibanaRequest({ routeAuthRequired: false })) + ).resolves.toEqual(AuthenticationResult.notHandled()); + }); + + it('does not handle authentication via `authorization` header.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe(authorization); + }); + + it('does not handle authentication via `authorization` header even if state exists.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); + await expect(provider.authenticate(request, {})).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe(authorization); + }); + + it('succeeds for non-AJAX requests if state is available.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, {})).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization } }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + }); + + it('succeeds for AJAX requests if state is available.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, {})).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization } }) + ); + + expectAuthenticateCall(mockOptions.client, { + headers: { authorization, 'kbn-xsrf': 'xsrf' }, + }); + }); + + it('non-AJAX requests can start a new session.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: {}, authHeaders: { authorization } }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + }); + + it('fails if credentials are not valid.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const authenticationError = new Error('Forbidden'); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(request.headers).not.toHaveProperty('authorization'); + }); + + if (!useBasicCredentials) { + it('properly handles extended format for the ApiKey credentials', async () => { + provider = new AnonymousAuthenticationProvider(mockOptions, { + credentials: { apiKey: { id: 'some-id', key: 'some-key' } }, + }); + authorization = new HTTPAuthorizationHeader( + 'ApiKey', + new BasicHTTPAuthorizationHeaderCredentials('some-id', 'some-key').toString() + ).toString(); + + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, {})).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization } }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + }); + } + }); + + describe('`logout` method', () => { + it('does not handle logout if state is not present', async () => { + await expect(provider.logout(httpServerMock.createKibanaRequest())).resolves.toEqual( + DeauthenticationResult.notHandled() + ); + }); + + it('always redirects to the logged out page.', async () => { + await expect(provider.logout(httpServerMock.createKibanaRequest(), {})).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out') + ); + + await expect( + provider.logout(httpServerMock.createKibanaRequest(), null) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out') + ); + }); + }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe( + useBasicCredentials ? 'basic' : 'apikey' + ); + }); + }); + } +}); diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.ts new file mode 100644 index 0000000000000..6f02cce371a41 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { canRedirectRequest } from '../can_redirect_request'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { + BasicHTTPAuthorizationHeaderCredentials, + HTTPAuthorizationHeader, +} from '../http_authentication'; +import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; + +/** + * Credentials that are based on the username and password. + */ +interface UsernameAndPasswordCredentials { + username: string; + password: string; +} + +/** + * Credentials that are based on the Elasticsearch API key. + */ +interface APIKeyCredentials { + apiKey: { id: string; key: string } | string; +} + +/** + * Checks whether current request can initiate a new session. + * @param request Request instance. + */ +function canStartNewSession(request: KibanaRequest) { + // We should try to establish new session only if request requires authentication and it's not XHR request. + // Technically we can authenticate XHR requests too, but we don't want these to create a new session unintentionally. + return canRedirectRequest(request) && request.route.options.authRequired === true; +} + +/** + * Checks whether specified `credentials` define an API key. + * @param credentials + */ +function isAPIKeyCredentials( + credentials: UsernameAndPasswordCredentials | APIKeyCredentials +): credentials is APIKeyCredentials { + return !!(credentials as APIKeyCredentials).apiKey; +} + +/** + * Provider that supports anonymous request authentication. + */ +export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider { + /** + * Type of the provider. + */ + static readonly type = 'anonymous'; + + /** + * Defines HTTP authorization header that should be used to authenticate request. + */ + private readonly httpAuthorizationHeader: HTTPAuthorizationHeader; + + constructor( + protected readonly options: Readonly<AuthenticationProviderOptions>, + anonymousOptions?: Readonly<{ + credentials?: Readonly<UsernameAndPasswordCredentials | APIKeyCredentials>; + }> + ) { + super(options); + + const credentials = anonymousOptions?.credentials; + if (!credentials) { + throw new Error('Credentials must be specified'); + } + + 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() + ); + } + } + + /** + * Performs initial login request. + * @param request Request instance. + * @param state Optional state value previously stored by the provider. + */ + public async login(request: KibanaRequest, state?: unknown) { + this.logger.debug('Trying to perform a login.'); + return this.authenticateViaAuthorizationHeader(request, state); + } + + /** + * Performs request authentication. + * @param request Request instance. + * @param state Optional state value previously stored by the provider. + */ + public async authenticate(request: KibanaRequest, state?: unknown) { + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); + + if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); + } + + if (state || canStartNewSession(request)) { + return this.authenticateViaAuthorizationHeader(request, state); + } + + return AuthenticationResult.notHandled(); + } + + /** + * Redirects user to the logged out page. + * @param request Request instance. + * @param state Optional state value previously stored by the provider. + */ + public async logout(request: KibanaRequest, state?: unknown) { + this.logger.debug( + `Logout is initiated by request to ${request.url.pathname}${request.url.search}.` + ); + + // Having a `null` state means that provider was specifically called to do a logout, but when + // session isn't defined then provider is just being probed whether or not it can perform logout. + if (state === undefined) { + return DeauthenticationResult.notHandled(); + } + + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + } + + /** + * Returns HTTP authentication scheme (`Basic` or `ApiKey`) that's used within `Authorization` + * HTTP header that provider attaches to all successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return this.httpAuthorizationHeader.scheme.toLowerCase(); + } + + /** + * Tries to authenticate user request via configured credentials encoded into `Authorization` header. + * @param request Request instance. + * @param state State value previously stored by the provider. + */ + private async authenticateViaAuthorizationHeader(request: KibanaRequest, state?: unknown) { + const authHeaders = { authorization: this.httpAuthorizationHeader.toString() }; + try { + const user = await this.getUser(request, authHeaders); + this.logger.debug( + `Request to ${request.url.pathname}${request.url.search} has been authenticated.` + ); + // Create session only if it doesn't exist yet, otherwise keep it unchanged. + return AuthenticationResult.succeeded(user, { authHeaders, state: state ? undefined : {} }); + } catch (err) { + this.logger.debug(`Failed to authenticate request : ${err.message}`); + return AuthenticationResult.failed(err); + } + } +} diff --git a/x-pack/plugins/security/server/authentication/providers/index.ts b/x-pack/plugins/security/server/authentication/providers/index.ts index 048afb6190d18..cfa9e71505066 100644 --- a/x-pack/plugins/security/server/authentication/providers/index.ts +++ b/x-pack/plugins/security/server/authentication/providers/index.ts @@ -9,6 +9,7 @@ export { AuthenticationProviderOptions, AuthenticationProviderSpecificOptions, } from './base'; +export { AnonymousAuthenticationProvider } from './anonymous'; export { BasicAuthenticationProvider } from './basic'; export { KerberosAuthenticationProvider } from './kerberos'; export { SAMLAuthenticationProvider, SAMLLogin } from './saml'; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 76a6586e5af80..a306e701e4e8d 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -28,6 +28,7 @@ describe('config schema', () => { ], }, "providers": Object { + "anonymous": undefined, "basic": Object { "basic": Object { "accessAgreement": undefined, @@ -76,6 +77,7 @@ describe('config schema', () => { ], }, "providers": Object { + "anonymous": undefined, "basic": Object { "basic": Object { "accessAgreement": undefined, @@ -124,6 +126,7 @@ describe('config schema', () => { ], }, "providers": Object { + "anonymous": undefined, "basic": Object { "basic": Object { "accessAgreement": undefined, @@ -863,6 +866,253 @@ describe('config schema', () => { }); }); + describe('`anonymous` provider', () => { + it('requires `order`', () => { + expect(() => + ConfigSchema.validate({ + authc: { providers: { anonymous: { anonymous1: { enabled: true } } } }, + }) + ).toThrow( + '[authc.providers.1.anonymous.anonymous1.order]: expected value of type [number] but got [undefined]' + ); + }); + + it('requires `credentials`', () => { + expect(() => + ConfigSchema.validate({ + authc: { providers: { anonymous: { anonymous1: { order: 0 } } } }, + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[authc.providers]: types that failed validation: + - [authc.providers.0]: expected value of type [array] but got [Object] + - [authc.providers.1.anonymous.anonymous1.credentials]: expected at least one defined value but got [undefined]" + `); + }); + + it('requires both `username` and `password` in username/password `credentials`', () => { + expect(() => + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { anonymous1: { order: 0, credentials: { username: 'some-user' } } }, + }, + }, + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[authc.providers]: types that failed validation: + - [authc.providers.0]: expected value of type [array] but got [Object] + - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: + - [credentials.0.password]: expected value of type [string] but got [undefined] + - [credentials.1.apiKey]: expected at least one defined value but got [undefined]" + `); + + expect(() => + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { anonymous1: { order: 0, credentials: { password: 'some-pass' } } }, + }, + }, + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[authc.providers]: types that failed validation: + - [authc.providers.0]: expected value of type [array] but got [Object] + - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: + - [credentials.0.username]: expected value of type [string] but got [undefined] + - [credentials.1.apiKey]: expected at least one defined value but got [undefined]" + `); + }); + + it('can be successfully validated with username/password credentials', () => { + expect( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'some-user', password: 'some-pass' }, + }, + }, + }, + }, + }).authc.providers + ).toMatchInlineSnapshot(` + Object { + "anonymous": Object { + "anonymous1": Object { + "credentials": Object { + "password": "some-pass", + "username": "some-user", + }, + "description": "Continue as Guest", + "enabled": true, + "hint": "For anonymous users", + "icon": "globe", + "order": 0, + "session": Object { + "idleTimeout": null, + }, + "showInSelector": true, + }, + }, + } + `); + }); + + it('requires both `id` and `key` in extended `apiKey` format credentials', () => { + expect(() => + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { anonymous1: { order: 0, credentials: { apiKey: { id: 'some-id' } } } }, + }, + }, + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[authc.providers]: types that failed validation: + - [authc.providers.0]: expected value of type [array] but got [Object] + - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: + - [credentials.0.username]: expected value of type [string] but got [undefined] + - [credentials.1.apiKey]: types that failed validation: + - [credentials.apiKey.0.key]: expected value of type [string] but got [undefined] + - [credentials.apiKey.1]: expected value of type [string] but got [Object]" + `); + + expect(() => + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { order: 0, credentials: { apiKey: { key: 'some-key' } } }, + }, + }, + }, + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[authc.providers]: types that failed validation: + - [authc.providers.0]: expected value of type [array] but got [Object] + - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: + - [credentials.0.username]: expected value of type [string] but got [undefined] + - [credentials.1.apiKey]: types that failed validation: + - [credentials.apiKey.0.id]: expected value of type [string] but got [undefined] + - [credentials.apiKey.1]: expected value of type [string] but got [Object]" + `); + }); + + it('can be successfully validated with API keys credentials', () => { + expect( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { apiKey: 'some-API-key' }, + }, + }, + }, + }, + }).authc.providers + ).toMatchInlineSnapshot(` + Object { + "anonymous": Object { + "anonymous1": Object { + "credentials": Object { + "apiKey": "some-API-key", + }, + "description": "Continue as Guest", + "enabled": true, + "hint": "For anonymous users", + "icon": "globe", + "order": 0, + "session": Object { + "idleTimeout": null, + }, + "showInSelector": true, + }, + }, + } + `); + + expect( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { apiKey: { id: 'some-id', key: 'some-key' } }, + }, + }, + }, + }, + }).authc.providers + ).toMatchInlineSnapshot(` + Object { + "anonymous": Object { + "anonymous1": Object { + "credentials": Object { + "apiKey": Object { + "id": "some-id", + "key": "some-key", + }, + }, + "description": "Continue as Guest", + "enabled": true, + "hint": "For anonymous users", + "icon": "globe", + "order": 0, + "session": Object { + "idleTimeout": null, + }, + "showInSelector": true, + }, + }, + } + `); + }); + + it('can be successfully validated with session config overrides', () => { + expect( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 1, + credentials: { username: 'some-user', password: 'some-pass' }, + session: { idleTimeout: 321, lifespan: 546 }, + }, + }, + }, + }, + }).authc.providers + ).toMatchInlineSnapshot(` + Object { + "anonymous": Object { + "anonymous1": Object { + "credentials": Object { + "password": "some-pass", + "username": "some-user", + }, + "description": "Continue as Guest", + "enabled": true, + "hint": "For anonymous users", + "icon": "globe", + "order": 1, + "session": Object { + "idleTimeout": "PT0.321S", + "lifespan": "PT0.546S", + }, + "showInSelector": true, + }, + }, + } + `); + }); + }); + it('`name` should be unique across all provider types', () => { expect(() => ConfigSchema.validate({ @@ -1623,5 +1873,113 @@ describe('createConfig()', () => { } `); }); + + it('properly handles config for the anonymous provider', async () => { + expect( + createMockConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'some-user', password: 'some-pass' }, + }, + }, + }, + }, + }).session.getExpirationTimeouts({ type: 'anonymous', name: 'anonymous1' }) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": null, + "lifespan": "P30D", + } + `); + + expect( + createMockConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'some-user', password: 'some-pass' }, + }, + }, + }, + }, + session: { idleTimeout: 0, lifespan: null }, + }).session.getExpirationTimeouts({ type: 'anonymous', name: 'anonymous1' }) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": null, + "lifespan": null, + } + `); + + expect( + createMockConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'some-user', password: 'some-pass' }, + session: { idleTimeout: 0, lifespan: null }, + }, + }, + }, + }, + }).session.getExpirationTimeouts({ type: 'anonymous', name: 'anonymous1' }) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": null, + "lifespan": null, + } + `); + + expect( + createMockConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'some-user', password: 'some-pass' }, + session: { idleTimeout: 321, lifespan: 546 }, + }, + }, + }, + }, + session: { idleTimeout: null, lifespan: 0 }, + }).session.getExpirationTimeouts({ type: 'anonymous', name: 'anonymous1' }) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": "PT0.321S", + "lifespan": "PT0.546S", + } + `); + + expect( + createMockConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'some-user', password: 'some-pass' }, + session: { idleTimeout: 321, lifespan: 546 }, + }, + }, + }, + }, + session: { idleTimeout: 123, lifespan: 456 }, + }).session.getExpirationTimeouts({ type: 'anonymous', name: 'anonymous1' }) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": "PT0.321S", + "lifespan": "PT0.546S", + } + `); + }); }); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index f44c68588fd61..b46c8dc2178a4 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -51,18 +51,27 @@ function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonCon }; } -function getUniqueProviderSchema( +function getUniqueProviderSchema<TProperties extends Record<string, Type<any>>>( providerType: string, - overrides?: Partial<ProvidersCommonConfigType> + overrides?: Partial<ProvidersCommonConfigType>, + properties?: TProperties ) { return schema.maybe( - schema.recordOf(schema.string(), schema.object(getCommonProviderSchemaProperties(overrides)), { - validate(config) { - if (Object.values(config).filter((provider) => provider.enabled).length > 1) { - return `Only one "${providerType}" provider can be configured.`; - } - }, - }) + schema.recordOf( + schema.string(), + schema.object( + properties + ? { ...getCommonProviderSchemaProperties(overrides), ...properties } + : getCommonProviderSchemaProperties(overrides) + ), + { + validate(config) { + if (Object.values(config).filter((provider) => provider.enabled).length > 1) { + return `Only one "${providerType}" provider can be configured.`; + } + }, + } + ) ); } @@ -120,6 +129,40 @@ const providersConfigSchema = schema.object( schema.object({ ...getCommonProviderSchemaProperties(), realm: schema.string() }) ) ), + anonymous: getUniqueProviderSchema( + 'anonymous', + { + description: schema.string({ + defaultValue: i18n.translate('xpack.security.loginAsGuestLabel', { + defaultMessage: 'Continue as Guest', + }), + }), + hint: schema.string({ + defaultValue: i18n.translate('xpack.security.loginAsGuestHintLabel', { + defaultMessage: 'For anonymous users', + }), + }), + icon: schema.string({ defaultValue: 'globe' }), + session: schema.object({ + idleTimeout: schema.nullable(schema.duration()), + lifespan: schema.maybe(schema.oneOf([schema.duration(), schema.literal(null)])), + }), + }, + { + credentials: schema.oneOf([ + schema.object({ + username: schema.string(), + password: schema.string(), + }), + schema.object({ + apiKey: schema.oneOf([ + schema.object({ id: schema.string(), key: schema.string() }), + schema.string(), + ]), + }), + ]), + } + ), }, { validate(config) { @@ -196,6 +239,7 @@ export const ConfigSchema = schema.object({ oidc: undefined, pki: undefined, kerberos: undefined, + anonymous: undefined, }, }), oidc: providerOptionsSchema('oidc', schema.object({ realm: schema.string() })), @@ -335,6 +379,7 @@ export function createConfig( } function getSessionConfig(session: RawConfigType['session'], providers: ProvidersConfigType) { + const defaultAnonymousSessionLifespan = schema.duration().validate('30d'); return { cleanupInterval: session.cleanupInterval, getExpirationTimeouts({ type, name }: AuthenticationProvider) { @@ -343,9 +388,20 @@ function getSessionConfig(session: RawConfigType['session'], providers: Provider // provider doesn't override session config and we should fall back to the global one instead. const providerSessionConfig = providers[type as keyof ProvidersConfigType]?.[name]?.session; + // We treat anonymous sessions differently since users can create them without realizing it. This may lead to a + // non controllable amount of sessions stored in the session index. To reduce the impact we set a 30 days lifespan + // for the anonymous sessions in case neither global nor provider specific lifespan is configured explicitly. + // We can remove this code once https://github.com/elastic/kibana/issues/68885 is resolved. + const providerLifespan = + type === 'anonymous' && + providerSessionConfig?.lifespan === undefined && + session.lifespan === undefined + ? defaultAnonymousSessionLifespan + : providerSessionConfig?.lifespan; + const [idleTimeout, lifespan] = [ [session.idleTimeout, providerSessionConfig?.idleTimeout], - [session.lifespan, providerSessionConfig?.lifespan], + [session.lifespan, providerLifespan], ].map(([globalTimeout, providerTimeout]) => { const timeout = providerTimeout === undefined ? globalTimeout ?? null : providerTimeout; return timeout && timeout.asMilliseconds() > 0 ? timeout : null; diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index b90a44be7aade..11b2cdcac021b 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -185,7 +185,7 @@ describe('Login view routes', () => { requiresSecureConnection: false, selector: { enabled: false, - providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }], + providers: [{ name: 'basic', type: 'basic', usesLoginForm: true, showInSelector: true }], }, }; await expect( @@ -209,7 +209,7 @@ describe('Login view routes', () => { requiresSecureConnection: false, selector: { enabled: false, - providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }], + providers: [{ name: 'basic', type: 'basic', usesLoginForm: true, showInSelector: true }], }, }; await expect( @@ -253,6 +253,7 @@ describe('Login view routes', () => { name: 'basic1', type: 'basic', usesLoginForm: true, + showInSelector: true, icon: 'logoElasticsearch', description: 'Log in with Elasticsearch', }, @@ -265,6 +266,7 @@ describe('Login view routes', () => { name: 'token1', type: 'token', usesLoginForm: true, + showInSelector: true, icon: 'logoElasticsearch', description: 'Log in with Elasticsearch', }, @@ -296,7 +298,7 @@ describe('Login view routes', () => { const contextMock = coreMock.createRequestHandlerContext(); const cases: Array<[ConfigType['authc'], LoginSelectorProvider[]]> = [ - // selector is disabled, multiple providers, but only basic provider should be returned. + // selector is disabled, multiple providers, all providers should be returned. [ getAuthcConfig({ selector: { enabled: false }, @@ -310,9 +312,16 @@ describe('Login view routes', () => { name: 'basic1', type: 'basic', usesLoginForm: true, + showInSelector: true, icon: 'logoElasticsearch', description: 'Log in with Elasticsearch', }, + { + type: 'saml', + name: 'saml1', + usesLoginForm: false, + showInSelector: false, + }, ], ], // selector is enabled, but only basic/token is available and should be returned. @@ -326,12 +335,13 @@ describe('Login view routes', () => { name: 'basic1', type: 'basic', usesLoginForm: true, + showInSelector: true, icon: 'logoElasticsearch', description: 'Log in with Elasticsearch', }, ], ], - // selector is enabled, all providers should be returned + // selector is enabled [ getAuthcConfig({ selector: { enabled: true }, @@ -345,7 +355,13 @@ describe('Login view routes', () => { }, }, saml: { - saml1: { order: 1, description: 'some-desc2', realm: 'realm1', icon: 'some-icon2' }, + saml1: { + order: 1, + description: 'some-desc2', + realm: 'realm1', + icon: 'some-icon2', + showInSelector: false, + }, saml2: { order: 2, description: 'some-desc3', hint: 'some-hint3', realm: 'realm2' }, }, }, @@ -358,6 +374,7 @@ describe('Login view routes', () => { hint: 'some-hint1', icon: 'logoElasticsearch', usesLoginForm: true, + showInSelector: true, }, { type: 'saml', @@ -365,6 +382,7 @@ describe('Login view routes', () => { description: 'some-desc2', icon: 'some-icon2', usesLoginForm: false, + showInSelector: false, }, { type: 'saml', @@ -372,55 +390,7 @@ describe('Login view routes', () => { description: 'some-desc3', hint: 'some-hint3', usesLoginForm: false, - }, - ], - ], - // selector is enabled, only providers that are enabled should be returned. - [ - getAuthcConfig({ - selector: { enabled: true }, - providers: { - basic: { - basic1: { - order: 0, - description: 'some-desc1', - hint: 'some-hint1', - icon: 'some-icon1', - }, - }, - saml: { - saml1: { - order: 1, - description: 'some-desc2', - realm: 'realm1', - showInSelector: false, - }, - saml2: { - order: 2, - description: 'some-desc3', - hint: 'some-hint3', - icon: 'some-icon3', - realm: 'realm2', - }, - }, - }, - }), - [ - { - type: 'basic', - name: 'basic1', - description: 'some-desc1', - hint: 'some-hint1', - icon: 'some-icon1', - usesLoginForm: true, - }, - { - type: 'saml', - name: 'saml2', - description: 'some-desc3', - hint: 'some-hint3', - icon: 'some-icon3', - usesLoginForm: false, + showInSelector: true, }, ], ], diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index f72facb2e24cc..93d43d04a86ca 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -55,18 +55,21 @@ export function defineLoginRoutes({ const { allowLogin, layout = 'form' } = license.getFeatures(); const { sortedProviders, selector } = config.authc; - const providers = []; - for (const { type, name } of sortedProviders) { + const providers = sortedProviders.map(({ type, name }) => { // Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can // be sure that config is present for every provider in `config.authc.sortedProviders`. const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!; - - // Include provider into the list if either selector is enabled or provider uses login form. const usesLoginForm = type === 'basic' || type === 'token'; - if (showInSelector && (usesLoginForm || selector.enabled)) { - providers.push({ type, name, usesLoginForm, description, hint, icon }); - } - } + return { + type, + name, + usesLoginForm, + showInSelector: showInSelector && (usesLoginForm || selector.enabled), + description, + hint, + icon, + }; + }); const loginState: LoginState = { allowLogin, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index 6be51d2a1adc2..26d2a2cff2910 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -40,9 +40,11 @@ export const getCreateSavedQueryRulesSchemaMock = (ruleId = 'rule-1'): SavedQuer }); export const getCreateThreatMatchRulesSchemaMock = ( - ruleId = 'rule-1' + ruleId = 'rule-1', + enabled = false ): ThreatMatchCreateSchema => ({ description: 'Detecting root and admin users', + enabled, name: 'Query with a rule id', query: 'user.name: root or user.name: admin', severity: 'high', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index 08c544b9246e0..1bf6b64db2427 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -115,12 +115,12 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R * Useful for e2e backend tests where it doesn't have date time and other * server side properties attached to it. */ -export const getThreatMatchingSchemaPartialMock = (): Partial<RulesSchema> => { +export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial<RulesSchema> => { return { author: [], created_by: 'elastic', description: 'Detecting root and admin users', - enabled: true, + enabled, false_positives: [], from: 'now-6m', immutable: false, diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 082f5100952ab..a4bdc4fc59a7c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -118,21 +118,29 @@ const APPLIED_POLICIES: Array<{ name: string; id: string; status: HostPolicyResponseActionStatus; + endpoint_policy_version: number; + version: number; }> = [ { name: 'Default', id: '00000000-0000-0000-0000-000000000000', status: HostPolicyResponseActionStatus.success, + endpoint_policy_version: 1, + version: 3, }, { name: 'With Eventing', id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A', status: HostPolicyResponseActionStatus.success, + endpoint_policy_version: 3, + version: 5, }, { name: 'Detect Malware Only', id: '47d7965d-6869-478b-bd9c-fb0d2bb3959f', status: HostPolicyResponseActionStatus.success, + endpoint_policy_version: 4, + version: 9, }, ]; @@ -251,6 +259,8 @@ interface HostInfo { id: string; status: HostPolicyResponseActionStatus; name: string; + endpoint_policy_version: number; + version: number; }; }; }; @@ -1332,7 +1342,7 @@ export class EndpointDocGenerator { allStatus?: HostPolicyResponseActionStatus; policyDataStream?: DataStream; } = {}): HostPolicyResponse { - const policyVersion = this.seededUUIDv4(); + const policyVersion = this.randomN(10); const status = () => { return allStatus || this.randomHostPolicyResponseActionStatus(); }; @@ -1501,6 +1511,8 @@ export class EndpointDocGenerator { status: this.commonInfo.Endpoint.policy.applied.status, version: policyVersion, name: this.commonInfo.Endpoint.policy.applied.name, + endpoint_policy_version: this.commonInfo.Endpoint.policy.applied + .endpoint_policy_version, }, }, }, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 66ba15431e603..f873a701eb9bd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -299,6 +299,8 @@ export interface HostResultList { request_page_index: number; /* the version of the query strategy */ query_strategy_version: MetadataQueryStrategyVersions; + /* policy IDs and versions */ + policy_info?: HostInfo['policy_info']; } /** @@ -520,9 +522,30 @@ export enum MetadataQueryStrategyVersions { VERSION_2 = 'v2', } +export type PolicyInfo = Immutable<{ + revision: number; + id: string; +}>; + export type HostInfo = Immutable<{ metadata: HostMetadata; host_status: HostStatus; + policy_info?: { + agent: { + /** + * As set in Kibana + */ + configured: PolicyInfo; + /** + * Last reported running in agent (may lag behind configured) + */ + applied: PolicyInfo; + }; + /** + * Current intended 'endpoint' package policy + */ + endpoint: PolicyInfo; + }; /* the version of the query strategy */ query_strategy_version: MetadataQueryStrategyVersions; }>; @@ -558,6 +581,8 @@ export type HostMetadata = Immutable<{ id: string; status: HostPolicyResponseActionStatus; name: string; + endpoint_policy_version: number; + version: number; }; }; }; @@ -1068,7 +1093,8 @@ export interface HostPolicyResponse { Endpoint: { policy: { applied: { - version: string; + version: number; + endpoint_policy_version: number; id: string; name: string; status: HostPolicyResponseActionStatus; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 3888d37a547f7..967b3870cb9e0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -401,3 +401,14 @@ export const importTimelineResultSchema = runtimeTypes.exact( export type ImportTimelineResultSchema = runtimeTypes.TypeOf<typeof importTimelineResultSchema>; export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom'; + +export interface TimelineExpandedEventType { + eventId: string; + indexName: string; + loading: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type EmptyObject = Record<any, never>; + +export type TimelineExpandedEvent = TimelineExpandedEventType | EmptyObject; 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 fb1f2920aaceb..596b92d064050 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 @@ -215,7 +215,8 @@ describe('Custom detection rules creation', () => { }); }); -describe('Custom detection rules deletion and edition', () => { +// FLAKY: https://github.com/elastic/kibana/issues/83772 +describe.skip('Custom detection rules deletion and edition', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index eb8448233c624..c2be6b2883c88 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,7 +17,8 @@ import { DETECTIONS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -describe('Export rules', () => { +// FLAKY: https://github.com/elastic/kibana/issues/69849 +describe.skip('Export rules', () => { before(() => { esArchiverLoad('export_rule'); cy.server(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 6b1f3699d333a..dd01159e3029f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -10,10 +10,9 @@ export const openTimelineUsingToggle = () => { cy.get(TIMELINE_TOGGLE_BUTTON).click(); }; -export const openTimelineIfClosed = () => { +export const openTimelineIfClosed = () => cy.get(MAIN_PAGE).then(($page) => { if ($page.find(TIMELINE_TOGGLE_BUTTON).length === 1) { openTimelineUsingToggle(); } }); -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index c54bd8b621d83..859ba3d1a0951 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; -import { CommentRequest, CommentType } from '../../../../../case/common/api'; +import { CommentType } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; @@ -16,7 +16,7 @@ import { useInsertTimeline } from '../../../timelines/components/timeline/insert import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; import * as i18n from './translations'; -import { schema } from './schema'; +import { schema, AddCommentFormSchema } from './schema'; import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click'; const MySpinner = styled(EuiLoadingSpinner)` @@ -25,9 +25,8 @@ const MySpinner = styled(EuiLoadingSpinner)` left: 50%; `; -const initialCommentValue: CommentRequest = { +const initialCommentValue: AddCommentFormSchema = { comment: '', - type: CommentType.user, }; export interface AddCommentRefObject { @@ -47,7 +46,7 @@ export const AddComment = React.memo( ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => { const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm<CommentRequest>({ + const { form } = useForm<AddCommentFormSchema>({ defaultValue: initialCommentValue, options: { stripEmptyFields: false }, schema, diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx index eb11357cd7ce9..5f244d64701fe 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CommentRequest } from '../../../../../case/common/api'; +import { CommentRequestUserType } from '../../../../../case/common/api'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; import * as i18n from './translations'; const { emptyField } = fieldValidators; -export const schema: FormSchema<CommentRequest> = { +export interface AddCommentFormSchema { + comment: CommentRequestUserType['comment']; +} + +export const schema: FormSchema<AddCommentFormSchema> = { comment: { type: FIELD_TYPES.TEXTAREA, validations: [ diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index de3e9c07ae8a3..228f3a4319c33 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -380,7 +380,7 @@ export const UserActionTree = React.memo( ]; } - // description, comments, tags + // title, description, comments, tags if ( action.actionField.length === 1 && ['title', 'description', 'comment', 'tags'].includes(action.actionField[0]) diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index 0d5bf13cd6261..0d2df7c2de3ea 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -348,6 +348,7 @@ describe('Case Configuration API', () => { method: 'PATCH', body: JSON.stringify({ comment: 'updated comment', + type: CommentType.user, id: basicCase.comments[0].id, version: basicCase.comments[0].version, }), @@ -404,7 +405,7 @@ describe('Case Configuration API', () => { }); const data = { comment: 'comment', - type: CommentType.user, + type: CommentType.user as const, }; test('check url, method, signal', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 83ee10e9b45a8..6046c3716b3b5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -11,13 +11,14 @@ import { CasePatchRequest, CasePostRequest, CasesStatusResponse, - CommentRequest, + CommentRequestUserType, User, CaseUserActionsResponse, CaseExternalServiceRequest, ServiceConnectorCaseParams, ServiceConnectorCaseResponse, ActionTypeExecutorResult, + CommentType, } from '../../../../case/common/api'; import { @@ -181,7 +182,7 @@ export const patchCasesStatus = async ( }; export const postComment = async ( - newComment: CommentRequest, + newComment: CommentRequestUserType, caseId: string, signal: AbortSignal ): Promise<Case> => { @@ -205,7 +206,12 @@ export const patchComment = async ( ): Promise<Case> => { const response = await KibanaServices.get().http.fetch<CaseResponse>(getCaseCommentsUrl(caseId), { method: 'PATCH', - body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), + body: JSON.stringify({ + comment: commentUpdate, + type: CommentType.user, + id: commentId, + version, + }), signal, }); return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index c2ddcce8b1d3c..b9db356498a01 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -19,7 +19,7 @@ export interface Comment { createdAt: string; createdBy: ElasticUser; comment: string; - type: CommentType; + type: CommentType.user; pushedAt: string | null; pushedBy: string | null; updatedAt: string | null; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index 773d4b8d1fe56..39ee21f942cbd 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -17,7 +17,7 @@ describe('usePostComment', () => { const abortCtrl = new AbortController(); const samplePost = { comment: 'a comment', - type: CommentType.user, + type: CommentType.user as const, }; const updateCaseCallback = jest.fn(); beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index e6cb8a9c3d150..cd3827a2887fb 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { CommentRequest } from '../../../../case/common/api'; +import { CommentRequestUserType } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postComment } from './api'; @@ -42,7 +42,7 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta }; export interface UsePostComment extends NewCommentState { - postComment: (data: CommentRequest, updateCase: (newCase: Case) => void) => void; + postComment: (data: CommentRequestUserType, updateCase: (newCase: Case) => void) => void; } export const usePostComment = (caseId: string): UsePostComment => { @@ -53,7 +53,7 @@ export const usePostComment = (caseId: string): UsePostComment => { const [, dispatchToaster] = useStateToaster(); const postMyComment = useCallback( - async (data: CommentRequest, updateCase: (newCase: Case) => void) => { + async (data: CommentRequestUserType, updateCase: (newCase: Case) => void) => { let cancel = false; const abortCtrl = new AbortController(); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 2ae621e71a725..9ca9cd6cce389 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -1544,12 +1544,5 @@ In other use cases the message field can be used to concatenate different values ] } /> - <CollapseLink - aria-label="Collapse" - data-test-subj="collapse" - onClick={[MockFunction]} - > - Collapse event - </CollapseLink> </Details> `; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 7b6e9fb21a3e3..35cb8f7b1c91f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -12,8 +12,8 @@ import { EuiFlexItem, EuiIcon, EuiPanel, - EuiText, EuiToolTip, + EuiIconTip, } from '@elastic/eui'; import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; @@ -27,7 +27,6 @@ import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; import { DraggableFieldBadge } from '../draggables/field_badge'; import { FieldName } from '../../../timelines/components/fields_browser/field_name'; -import { SelectableText } from '../selectable_text'; import { OverflowField } from '../tables/helpers'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; @@ -90,6 +89,21 @@ export const getColumns = ({ </EuiToolTip> ), }, + { + field: 'description', + name: '', + render: (description: string | null | undefined, data: EventFieldsData) => ( + <EuiIconTip + aria-label={i18n.DESCRIPTION} + type="iInCircle" + color="subdued" + content={`${description || ''} ${getExampleText(data.example)}`} + /> + ), + sortable: true, + truncateText: true, + width: '30px', + }, { field: 'field', name: i18n.FIELD, @@ -187,18 +201,6 @@ export const getColumns = ({ </EuiFlexGroup> ), }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: string | null | undefined, data: EventFieldsData) => ( - <SelectableText> - <EuiText size="xs">{`${description || ''} ${getExampleText(data.example)}`}</EuiText> - </SelectableText> - ), - sortable: true, - truncateText: true, - width: '50%', - }, { field: 'valuesConcatenated', name: i18n.BLANK, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index c3c7c864ac99b..bafe3df1a9cc7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -23,14 +23,12 @@ import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../link_to'); describe('EventDetails', () => { const mount = useMountAppended(); - const onEventToggled = jest.fn(); const defaultProps = { browserFields: mockBrowserFields, columnHeaders: defaultHeaders, data: mockDetailItemData, id: mockDetailItemDataId, view: 'table-view' as View, - onEventToggled, onUpdateColumns: jest.fn(), onViewSelected: jest.fn(), timelineId: 'test', @@ -66,12 +64,5 @@ describe('EventDetails', () => { wrapper.find('[data-test-subj="eventDetails"]').find('.euiTab-isSelected').first().text() ).toEqual('Table'); }); - - test('it invokes `onEventToggled` when the collapse button is clicked', () => { - wrapper.find('[data-test-subj="collapse"]').first().simulate('click'); - wrapper.update(); - - expect(onEventToggled).toHaveBeenCalled(); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 074e6faf80c7d..a2a7182a768cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -5,7 +5,7 @@ */ import { EuiLink, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -15,9 +15,12 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; -import { COLLAPSE, COLLAPSE_EVENT } from '../../../timelines/components/timeline/body/translations'; -export type View = 'table-view' | 'json-view'; +export type View = EventsViewType.tableView | EventsViewType.jsonView; +export enum EventsViewType { + tableView = 'table-view', + jsonView = 'json-view', +} const CollapseLink = styled(EuiLink)` margin: 20px 0; @@ -30,10 +33,9 @@ interface Props { columnHeaders: ColumnHeaderOptions[]; data: TimelineEventsDetailsItem[]; id: string; - view: View; - onEventToggled: () => void; + view: EventsViewType; onUpdateColumns: OnUpdateColumns; - onViewSelected: (selected: View) => void; + onViewSelected: (selected: EventsViewType) => void; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; } @@ -51,16 +53,19 @@ export const EventDetails = React.memo<Props>( data, id, view, - onEventToggled, onUpdateColumns, onViewSelected, timelineId, toggleColumn, }) => { + const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [ + onViewSelected, + ]); + const tabs: EuiTabbedContentTab[] = useMemo( () => [ { - id: 'table-view', + id: EventsViewType.tableView, name: i18n.TABLE, content: ( <EventFieldsBrowser @@ -75,7 +80,7 @@ export const EventDetails = React.memo<Props>( ), }, { - id: 'json-view', + id: EventsViewType.jsonView, name: i18n.JSON_VIEW, content: <JsonView data={data} />, }, @@ -88,11 +93,8 @@ export const EventDetails = React.memo<Props>( <EuiTabbedContent tabs={tabs} selectedTab={view === 'table-view' ? tabs[0] : tabs[1]} - onTabClick={(e) => onViewSelected(e.id as View)} + onTabClick={handleTabClick} /> - <CollapseLink aria-label={COLLAPSE} data-test-subj="collapse" onClick={onEventToggled}> - {COLLAPSE_EVENT} - </CollapseLink> </Details> ); } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 77d0ec330476c..0acf461828bc3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -21,7 +21,7 @@ describe('EventFieldsBrowser', () => { const mount = useMountAppended(); describe('column headers', () => { - ['Field', 'Value', 'Description'].forEach((header) => { + ['Field', 'Value'].forEach((header) => { test(`it renders the ${header} column header`, () => { const wrapper = mount( <TestProviders> @@ -229,8 +229,15 @@ describe('EventFieldsBrowser', () => { </TestProviders> ); - expect(wrapper.find('.euiTableRow').find('.euiTableRowCell').at(3).text()).toContain( - 'DescriptionDate/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' + expect( + wrapper + .find('.euiTableRow') + .find('.euiTableRowCell') + .at(1) + .find('EuiIconTip') + .prop('content') + ).toContain( + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx index bb74935d5703e..4730dc5c2264f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx @@ -4,49 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { BrowserFields } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; -import { EventDetails, View } from './event_details'; +import { EventDetails, EventsViewType, View } from './event_details'; interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; data: TimelineEventsDetailsItem[]; id: string; - onEventToggled: () => void; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; } export const StatefulEventDetails = React.memo<Props>( - ({ - browserFields, - columnHeaders, - data, - id, - onEventToggled, - onUpdateColumns, - timelineId, - toggleColumn, - }) => { - const [view, setView] = useState<View>('table-view'); + ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { + // TODO: Move to the store + const [view, setView] = useState<View>(EventsViewType.tableView); - const handleSetView = useCallback((newView) => setView(newView), []); return ( <EventDetails browserFields={browserFields} columnHeaders={columnHeaders} data={data} id={id} - onEventToggled={onEventToggled} onUpdateColumns={onUpdateColumns} - onViewSelected={handleSetView} + onViewSelected={setView} timelineId={timelineId} toggleColumn={toggleColumn} view={view} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx new file mode 100644 index 0000000000000..ad332b2759048 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; + +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { BrowserFields, DocValueFields } from '../../containers/source'; +import { + ExpandableEvent, + ExpandableEventTitle, +} from '../../../timelines/components/timeline/expandable_event'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; + +const StyledEuiFlyout = styled(EuiFlyout)` + z-index: 9999; +`; + +interface EventDetailsFlyoutProps { + browserFields: BrowserFields; + docValueFields: DocValueFields[]; + timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; +} + +const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({ + browserFields, + docValueFields, + timelineId, + toggleColumn, +}) => { + const dispatch = useDispatch(); + const expandedEvent = useDeepEqualSelector( + (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? {} + ); + + const handleClearSelection = useCallback(() => { + dispatch( + timelineActions.toggleExpandedEvent({ + timelineId, + event: {}, + }) + ); + }, [dispatch, timelineId]); + + if (!expandedEvent.eventId) { + return null; + } + + return ( + <StyledEuiFlyout size="s" onClose={handleClearSelection}> + <EuiFlyoutHeader hasBorder> + <ExpandableEventTitle /> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <ExpandableEvent + browserFields={browserFields} + docValueFields={docValueFields} + event={expandedEvent} + timelineId={timelineId} + toggleColumn={toggleColumn} + /> + </EuiFlyoutBody> + </StyledEuiFlyout> + ); +}; + +export const EventDetailsFlyout = React.memo( + EventDetailsFlyoutComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn +); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 421b111d7941f..186083f1b05cd 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -36,7 +36,8 @@ import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; import { useFullScreen } from '../../containers/use_full_screen'; -import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../common/types/timeline'; +import { GraphOverlay } from '../../../timelines/components/graph_overlay'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px @@ -76,6 +77,16 @@ const EventsContainerLoading = styled.div` flex-direction: column; `; +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + width: 100%; + overflow: hidden; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + /** * Hides stateful headerFilterGroup implementations, but prevents the component * from being unmounted, to preserve the state of the component @@ -280,21 +291,27 @@ const EventsViewerComponent: React.FC<Props> = ({ refetch={refetch} /> - <StatefulBody - browserFields={browserFields} - data={nonDeletedEvents} - docValueFields={docValueFields} - id={id} - isEventViewer={true} - onRuleChange={onRuleChange} - refetch={refetch} - sort={sort} - toggleColumn={toggleColumn} - /> - - { - /** Hide the footer if Resolver is showing. */ - !graphEventId && ( + {graphEventId && ( + <GraphOverlay + graphEventId={graphEventId} + isEventViewer={true} + timelineId={id} + timelineType={TimelineType.default} + /> + )} + <FullWidthFlexGroup $visible={!graphEventId}> + <ScrollableFlexItem grow={1}> + <StatefulBody + browserFields={browserFields} + data={nonDeletedEvents} + docValueFields={docValueFields} + id={id} + isEventViewer={true} + onRuleChange={onRuleChange} + refetch={refetch} + sort={sort} + toggleColumn={toggleColumn} + /> <Footer activePage={pageInfo.activePage} data-test-subj="events-viewer-footer" @@ -310,8 +327,8 @@ const EventsViewerComponent: React.FC<Props> = ({ onChangePage={loadPage} totalCount={totalCountMinusDeleted} /> - ) - } + </ScrollableFlexItem> + </FullWidthFlexGroup> </EventsContainerLoading> </> </EventDetailsWidthProvider> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index a4f2b0536abf5..58f81c9fb3c8b 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -24,6 +24,7 @@ import { InspectButtonContainer } from '../inspect'; import { useFullScreen } from '../../containers/use_full_screen'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; +import { EventDetailsFlyout } from './event_details_flyout'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; @@ -134,36 +135,44 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); return ( - <FullScreenContainer $isFullScreen={globalFullScreen}> - <InspectButtonContainer> - <EventsViewer - browserFields={browserFields} - columns={columns} - docValueFields={docValueFields} - id={id} - dataProviders={dataProviders!} - deletedEventIds={deletedEventIds} - end={end} - isLoadingIndexPattern={isLoadingIndexPattern} - filters={globalFilters} - headerFilterGroup={headerFilterGroup} - indexNames={selectedPatterns} - indexPattern={indexPattern} - isLive={isLive} - itemsPerPage={itemsPerPage!} - itemsPerPageOptions={itemsPerPageOptions!} - kqlMode={kqlMode} - onChangeItemsPerPage={onChangeItemsPerPage} - query={query} - onRuleChange={onRuleChange} - start={start} - sort={sort} - toggleColumn={toggleColumn} - utilityBar={utilityBar} - graphEventId={graphEventId} - /> - </InspectButtonContainer> - </FullScreenContainer> + <> + <FullScreenContainer $isFullScreen={globalFullScreen}> + <InspectButtonContainer> + <EventsViewer + browserFields={browserFields} + columns={columns} + docValueFields={docValueFields} + id={id} + dataProviders={dataProviders!} + deletedEventIds={deletedEventIds} + end={end} + isLoadingIndexPattern={isLoadingIndexPattern} + filters={globalFilters} + headerFilterGroup={headerFilterGroup} + indexNames={selectedPatterns} + indexPattern={indexPattern} + isLive={isLive} + itemsPerPage={itemsPerPage!} + itemsPerPageOptions={itemsPerPageOptions!} + kqlMode={kqlMode} + onChangeItemsPerPage={onChangeItemsPerPage} + query={query} + onRuleChange={onRuleChange} + start={start} + sort={sort} + toggleColumn={toggleColumn} + utilityBar={utilityBar} + graphEventId={graphEventId} + /> + </InspectButtonContainer> + </FullScreenContainer> + <EventDetailsFlyout + browserFields={browserFields} + docValueFields={docValueFields} + timelineId={id} + toggleColumn={toggleColumn} + /> + </> ); }; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 0944b6aa27f67..ba375612b22a7 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMELINE_WIDTH } from '../../timelines/components/timeline/body/constants'; import { Direction, FlowTarget, @@ -213,6 +212,7 @@ export const mockGlobalState: State = { description: '', eventIdToNoteIds: {}, excludedRowRendererIds: [], + expandedEvent: {}, highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -238,7 +238,6 @@ export const mockGlobalState: State = { pinnedEventsSaveObject: {}, itemsPerPageOptions: [5, 10, 20], sort: { columnId: '@timestamp', sortDirection: Direction.desc }, - width: DEFAULT_TIMELINE_WIDTH, isSaving: false, version: null, status: TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index ed226fb0c984f..0118004b48eb8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2100,6 +2100,7 @@ export const mockTimelineModel: TimelineModel = { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [ { $state: { @@ -2150,7 +2151,6 @@ export const mockTimelineModel: TimelineModel = { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }; export const mockTimelineResult: TimelineResult = { @@ -2220,6 +2220,7 @@ export const defaultTimelineProps: CreateTimelineProps = { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -2252,7 +2253,6 @@ export const defaultTimelineProps: CreateTimelineProps = { templateTimelineVersion: null, templateTimelineId: null, version: null, - width: 1100, }, to: '2018-11-05T19:03:25.937Z', notes: null, diff --git a/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx b/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx index 7246259f5afa1..ac8c78b1fdbd4 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx @@ -188,6 +188,9 @@ describe('Spy Routes', () => { }); wrapper.update(); expect(dispatchMock.mock.calls[0]).toEqual([ + { type: 'updateSearch', search: '?updated="true"' }, + ]); + expect(dispatchMock.mock.calls[1]).toEqual([ { route: { detailName: undefined, diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index febcf0aee679d..5450a6ec1a313 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -35,6 +35,11 @@ export const SpyRouteComponent = memo< search, }); setIsInitializing(false); + } else if (search !== '' && search !== route.search) { + dispatch({ + type: 'updateSearch', + search, + }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [search]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index ecc0fc54d0d47..6b7cc8167ede6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -190,6 +190,7 @@ describe('alert actions', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [ { $state: { @@ -253,7 +254,6 @@ describe('alert actions', () => { templateTimelineId: null, templateTimelineVersion: null, version: null, - width: 1100, }, to: '2018-11-05T19:03:25.937Z', ruleNote: '# this is some markdown documentation', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 84d1dabe86910..2e9206d945cad 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -63,6 +63,7 @@ describe('EndpointList store concerns', () => { agentsWithEndpointsTotalError: undefined, endpointsTotalError: undefined, queryStrategyVersion: undefined, + policyVersionInfo: undefined, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 26d8dda2f4aec..33772f4463543 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -41,6 +41,7 @@ export const initialEndpointListState: Immutable<EndpointState> = { endpointsTotal: 0, endpointsTotalError: undefined, queryStrategyVersion: undefined, + policyVersionInfo: undefined, }; /* eslint-disable-next-line complexity */ @@ -55,6 +56,7 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = ( request_page_size: pageSize, request_page_index: pageIndex, query_strategy_version: queryStrategyVersion, + policy_info: policyVersionInfo, } = action.payload; return { ...state, @@ -63,6 +65,7 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = ( pageSize, pageIndex, queryStrategyVersion, + policyVersionInfo, loading: false, error: undefined, }; @@ -104,6 +107,7 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = ( return { ...state, details: action.payload.metadata, + policyVersionInfo: action.payload.policy_info, detailsLoading: false, detailsError: undefined, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 29d9185b6cea5..1901f3589104a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -55,6 +55,8 @@ export const isAutoRefreshEnabled = (state: Immutable<EndpointState>) => state.i export const autoRefreshInterval = (state: Immutable<EndpointState>) => state.autoRefreshInterval; +export const policyVersionInfo = (state: Immutable<EndpointState>) => state.policyVersionInfo; + export const areEndpointsEnrolling = (state: Immutable<EndpointState>) => { return state.agentsWithEndpointsTotal > state.endpointsTotal; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index ec22c522c3d0a..63ec991ecf6d1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -76,6 +76,8 @@ export interface EndpointState { endpointsTotalError?: ServerApiError; /** The query strategy version that informs whether the transform for KQL is enabled or not */ queryStrategyVersion?: MetadataQueryStrategyVersions; + /** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */ + policyVersionInfo?: HostInfo['policy_info']; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts new file mode 100644 index 0000000000000..ce6d2f354cc45 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.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 { HostInfo, HostMetadata } from '../../../../common/endpoint/types'; + +export const isPolicyOutOfDate = ( + reported: HostMetadata['Endpoint']['policy']['applied'], + current: HostInfo['policy_info'] +): boolean => { + if (current === undefined || current === null) { + return false; // we don't know, can't declare it out-of-date + } + return !( + reported.id === current.endpoint.id && // endpoint package policy not reassigned + current.agent.configured.id === current.agent.applied.id && // agent policy wasn't reassigned and not-yet-applied + // all revisions match up + reported.version >= current.agent.applied.revision && + reported.version >= current.agent.configured.revision && + reported.endpoint_policy_version >= current.endpoint.revision + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/out_of_date.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/out_of_date.tsx new file mode 100644 index 0000000000000..6718dfe4cb9b4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/out_of_date.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiText, EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const OutOfDate = React.memo<{ style?: React.CSSProperties }>(({ style, ...otherProps }) => { + return ( + <EuiText color="subdued" size="xs" className="eui-textNoWrap" style={style} {...otherProps}> + <EuiIcon size="m" type="alert" color="warning" /> + <FormattedMessage id="xpack.securitySolution.outOfDateLabel" defaultMessage="Out-of-date" /> + </EuiText> + ); +}); + +OutOfDate.displayName = 'OutOfDate'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index dd7475361b950..dbb242845626e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -18,7 +18,8 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { HostMetadata } from '../../../../../../common/endpoint/types'; +import { isPolicyOutOfDate } from '../../utils'; +import { HostInfo, HostMetadata } from '../../../../../../common/endpoint/types'; import { useEndpointSelector, useAgentDetailsIngestUrl } from '../hooks'; import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; @@ -31,6 +32,7 @@ import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { AgentDetailsReassignPolicyAction } from '../../../../../../../fleet/public'; import { EndpointPolicyLink } from '../components/endpoint_policy_link'; +import { OutOfDate } from '../components/out_of_date'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -51,187 +53,190 @@ const LinkToExternalApp = styled.div` const openReassignFlyoutSearch = '?openReassignFlyout=true'; -export const EndpointDetails = memo(({ details }: { details: HostMetadata }) => { - const agentId = details.elastic.agent.id; - const { - url: agentDetailsUrl, - appId: ingestAppId, - appPath: agentDetailsAppPath, - } = useAgentDetailsIngestUrl(agentId); - const queryParams = useEndpointSelector(uiQueryParams); - const policyStatus = useEndpointSelector( - policyResponseStatus - ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; - const { formatUrl } = useFormatUrl(SecurityPageName.administration); +export const EndpointDetails = memo( + ({ details, policyInfo }: { details: HostMetadata; policyInfo?: HostInfo['policy_info'] }) => { + const agentId = details.elastic.agent.id; + const { + url: agentDetailsUrl, + appId: ingestAppId, + appPath: agentDetailsAppPath, + } = useAgentDetailsIngestUrl(agentId); + const queryParams = useEndpointSelector(uiQueryParams); + const policyStatus = useEndpointSelector( + policyResponseStatus + ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; + const { formatUrl } = useFormatUrl(SecurityPageName.administration); - const detailsResultsUpper = useMemo(() => { - return [ - { - title: i18n.translate('xpack.securitySolution.endpoint.details.os', { - defaultMessage: 'OS', - }), - description: details.host.os.full, - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', { - defaultMessage: 'Last Seen', - }), - description: <FormattedDateAndTime date={new Date(details['@timestamp'])} />, - }, - ]; - }, [details]); + const detailsResultsUpper = useMemo(() => { + return [ + { + title: i18n.translate('xpack.securitySolution.endpoint.details.os', { + defaultMessage: 'OS', + }), + description: details.host.os.full, + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', { + defaultMessage: 'Last Seen', + }), + description: <FormattedDateAndTime date={new Date(details['@timestamp'])} />, + }, + ]; + }, [details]); - const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { selected_endpoint, show, ...currentUrlParams } = queryParams; - return [ - formatUrl( + const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { selected_endpoint, show, ...currentUrlParams } = queryParams; + return [ + formatUrl( + getEndpointDetailsPath({ + name: 'endpointPolicyResponse', + ...currentUrlParams, + selected_endpoint: details.agent.id, + }) + ), getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...currentUrlParams, selected_endpoint: details.agent.id, - }) - ), - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', - ...currentUrlParams, - selected_endpoint: details.agent.id, - }), - ]; - }, [details.agent.id, formatUrl, queryParams]); + }), + ]; + }, [details.agent.id, formatUrl, queryParams]); + + const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`; + const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`; + const handleReassignEndpointsClick = useNavigateToAppEventHandler< + AgentDetailsReassignPolicyAction + >(ingestAppId, { + path: agentDetailsWithFlyoutPath, + state: { + onDoneNavigateTo: [ + 'securitySolution:administration', + { + path: getEndpointDetailsPath({ + name: 'endpointDetails', + selected_endpoint: details.agent.id, + }), + }, + ], + }, + }); - const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`; - const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`; - const handleReassignEndpointsClick = useNavigateToAppEventHandler< - AgentDetailsReassignPolicyAction - >(ingestAppId, { - path: agentDetailsWithFlyoutPath, - state: { - onDoneNavigateTo: [ - 'securitySolution:administration', + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); + + const detailsResultsPolicy = useMemo(() => { + return [ { - path: getEndpointDetailsPath({ - name: 'endpointDetails', - selected_endpoint: details.agent.id, + title: i18n.translate('xpack.securitySolution.endpoint.details.policy', { + defaultMessage: 'Integration Policy', }), + description: ( + <> + <EndpointPolicyLink + policyId={details.Endpoint.policy.applied.id} + data-test-subj="policyDetailsValue" + > + {details.Endpoint.policy.applied.name} + </EndpointPolicyLink> + {isPolicyOutOfDate(details.Endpoint.policy.applied, policyInfo) && <OutOfDate />} + </> + ), }, - ], - }, - }); - - const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); - - const detailsResultsPolicy = useMemo(() => { - return [ - { - title: i18n.translate('xpack.securitySolution.endpoint.details.policy', { - defaultMessage: 'Integration Policy', - }), - description: ( - <> - <EndpointPolicyLink - policyId={details.Endpoint.policy.applied.id} - data-test-subj="policyDetailsValue" - > - {details.Endpoint.policy.applied.name} - </EndpointPolicyLink> - </> - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', { - defaultMessage: 'Policy Response', - }), - description: ( - <EuiHealth - color={POLICY_STATUS_TO_HEALTH_COLOR[policyStatus] || 'subdued'} - data-test-subj="policyStatusHealth" - > - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - <EuiLink - data-test-subj="policyStatusValue" - href={policyResponseUri} - onClick={policyStatusClickHandler} + { + title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', { + defaultMessage: 'Policy Response', + }), + description: ( + <EuiHealth + color={POLICY_STATUS_TO_HEALTH_COLOR[policyStatus] || 'subdued'} + data-test-subj="policyStatusHealth" > - <EuiText size="m"> - <FormattedMessage - id="xpack.securitySolution.endpoint.details.policyStatusValue" - defaultMessage="{policyStatus, select, success {Success} warning {Warning} failure {Failed} other {Unknown}}" - values={{ policyStatus }} - /> - </EuiText> - </EuiLink> - </EuiHealth> - ), - }, - ]; - }, [details, policyResponseUri, policyStatus, policyStatusClickHandler]); - const detailsResultsLower = useMemo(() => { - return [ - { - title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', { - defaultMessage: 'IP Address', - }), - description: ( - <EuiListGroup flush> - {details.host.ip.map((ip: string, index: number) => ( - <HostIds key={index} label={ip} /> - ))} - </EuiListGroup> - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', { - defaultMessage: 'Hostname', - }), - description: details.host.hostname, - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', { - defaultMessage: 'Endpoint Version', - }), - description: details.agent.version, - }, - ]; - }, [details.agent.version, details.host.hostname, details.host.ip]); + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + <EuiLink + data-test-subj="policyStatusValue" + href={policyResponseUri} + onClick={policyStatusClickHandler} + > + <EuiText size="m"> + <FormattedMessage + id="xpack.securitySolution.endpoint.details.policyStatusValue" + defaultMessage="{policyStatus, select, success {Success} warning {Warning} failure {Failed} other {Unknown}}" + values={{ policyStatus }} + /> + </EuiText> + </EuiLink> + </EuiHealth> + ), + }, + ]; + }, [details, policyResponseUri, policyStatus, policyStatusClickHandler, policyInfo]); + const detailsResultsLower = useMemo(() => { + return [ + { + title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', { + defaultMessage: 'IP Address', + }), + description: ( + <EuiListGroup flush> + {details.host.ip.map((ip: string, index: number) => ( + <HostIds key={index} label={ip} /> + ))} + </EuiListGroup> + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', { + defaultMessage: 'Hostname', + }), + description: details.host.hostname, + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', { + defaultMessage: 'Endpoint Version', + }), + description: details.agent.version, + }, + ]; + }, [details.agent.version, details.host.hostname, details.host.ip]); - return ( - <> - <EuiDescriptionList - type="column" - listItems={detailsResultsUpper} - data-test-subj="endpointDetailsUpperList" - /> - <EuiHorizontalRule margin="m" /> - <EuiDescriptionList - type="column" - listItems={detailsResultsPolicy} - data-test-subj="endpointDetailsPolicyList" - /> - <LinkToExternalApp> - <LinkToApp - appId={ingestAppId} - appPath={agentDetailsWithFlyoutPath} - href={agentDetailsWithFlyoutUrl} - onClick={handleReassignEndpointsClick} - data-test-subj="endpointDetailsLinkToIngest" - > - <EuiIcon type="savedObjectsApp" className="linkToAppIcon" /> - <FormattedMessage - id="xpack.securitySolution.endpoint.details.linkToIngestTitle" - defaultMessage="Reassign Policy" - /> - <EuiIcon type="popout" className="linkToAppPopoutIcon" /> - </LinkToApp> - </LinkToExternalApp> - <EuiHorizontalRule margin="m" /> - <EuiDescriptionList - type="column" - listItems={detailsResultsLower} - data-test-subj="endpointDetailsLowerList" - /> - </> - ); -}); + return ( + <> + <EuiDescriptionList + type="column" + listItems={detailsResultsUpper} + data-test-subj="endpointDetailsUpperList" + /> + <EuiHorizontalRule margin="m" /> + <EuiDescriptionList + type="column" + listItems={detailsResultsPolicy} + data-test-subj="endpointDetailsPolicyList" + /> + <LinkToExternalApp> + <LinkToApp + appId={ingestAppId} + appPath={agentDetailsWithFlyoutPath} + href={agentDetailsWithFlyoutUrl} + onClick={handleReassignEndpointsClick} + data-test-subj="endpointDetailsLinkToIngest" + > + <EuiIcon type="savedObjectsApp" className="linkToAppIcon" /> + <FormattedMessage + id="xpack.securitySolution.endpoint.details.linkToIngestTitle" + defaultMessage="Reassign Policy" + /> + <EuiIcon type="popout" className="linkToAppPopoutIcon" /> + </LinkToApp> + </LinkToExternalApp> + <EuiHorizontalRule margin="m" /> + <EuiDescriptionList + type="column" + listItems={detailsResultsLower} + data-test-subj="endpointDetailsLowerList" + /> + </> + ); + } +); EndpointDetails.displayName = 'EndpointDetails'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 6bc3445c8e745..edc15e22a699e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -33,6 +33,7 @@ import { policyResponseError, policyResponseLoading, policyResponseTimestamp, + policyVersionInfo, } from '../../store/selectors'; import { EndpointDetails } from './endpoint_details'; import { PolicyResponse } from './policy_response'; @@ -53,6 +54,7 @@ export const EndpointDetailsFlyout = memo(() => { ...queryParamsWithoutSelectedEndpoint } = queryParams; const details = useEndpointSelector(detailsData); + const policyInfo = useEndpointSelector(policyVersionInfo); const loading = useEndpointSelector(detailsLoading); const error = useEndpointSelector(detailsError); const show = useEndpointSelector(showView); @@ -101,7 +103,7 @@ export const EndpointDetailsFlyout = memo(() => { {show === 'details' && ( <> <EuiFlyoutBody data-test-subj="endpointDetailsFlyoutBody"> - <EndpointDetails details={details} /> + <EndpointDetails details={details} policyInfo={policyInfo} /> </EuiFlyoutBody> </> )} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 4b955f2fe2959..69889d3d0a881 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -228,15 +228,58 @@ describe('when on the list page', () => { firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; - [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach( - (status, index) => { - hostListData[index] = { - metadata: hostListData[index].metadata, - host_status: status, - query_strategy_version: queryStrategyVersion, - }; - } - ); + // add ability to change (immutable) policy + type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> }; + type Policy = DeepMutable<NonNullable<HostInfo['policy_info']>>; + + const makePolicy = ( + applied: HostInfo['metadata']['Endpoint']['policy']['applied'], + cb: (policy: Policy) => Policy + ): Policy => { + return cb({ + agent: { + applied: { id: 'xyz', revision: applied.version }, + configured: { id: 'xyz', revision: applied.version }, + }, + endpoint: { id: applied.id, revision: applied.endpoint_policy_version }, + }); + }; + + [ + { status: HostStatus.ERROR, policy: (p: Policy) => p }, + { + status: HostStatus.ONLINE, + policy: (p: Policy) => { + p.endpoint.id = 'xyz'; // represents change in endpoint policy assignment + p.endpoint.revision = 1; + return p; + }, + }, + { + status: HostStatus.OFFLINE, + policy: (p: Policy) => { + p.endpoint.revision += 1; // changes made to endpoint policy + return p; + }, + }, + { + status: HostStatus.UNENROLLING, + policy: (p: Policy) => { + p.agent.configured.revision += 1; // agent policy change, not propagated to agent yet + return p; + }, + }, + ].forEach((setup, index) => { + hostListData[index] = { + metadata: hostListData[index].metadata, + host_status: setup.status, + policy_info: makePolicy( + hostListData[index].metadata.Endpoint.policy.applied, + setup.policy + ), + query_strategy_version: queryStrategyVersion, + }; + }); hostListData.forEach((item, index) => { generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status; }); @@ -316,6 +359,20 @@ describe('when on the list page', () => { }); }); + it('should display policy out-of-date warning when changes pending', async () => { + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedEndpointList'); + }); + const outOfDates = await renderResult.findAllByTestId('rowPolicyOutOfDate'); + expect(outOfDates).toHaveLength(3); + + outOfDates.forEach((item, index) => { + expect(item.textContent).toEqual('Out-of-date'); + expect(item.querySelector(`[data-euiicon-type][color=warning]`)).not.toBeNull(); + }); + }); + it('should display policy name as a link', async () => { const renderResult = render(); await reactTestingLibrary.act(async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 2b40a7507da88..492b50af3dbd7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -35,6 +35,7 @@ import { NavigateToAppOptions } from 'kibana/public'; import { EndpointDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; import { useEndpointSelector } from './hooks'; +import { isPolicyOutOfDate } from '../utils'; import { HOST_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_HEALTH_COLOR, @@ -57,6 +58,7 @@ import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/rou import { useFormatUrl } from '../../../../common/components/link_to'; import { EndpointAction } from '../store/action'; import { EndpointPolicyLink } from './components/endpoint_policy_link'; +import { OutOfDate } from './components/out_of_date'; import { AdminSearchBar } from './components/search_bar'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -322,17 +324,22 @@ export const EndpointList = () => { }), truncateText: true, // eslint-disable-next-line react/display-name - render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied']) => { + render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { return ( - <EuiToolTip content={policy.name} anchorClassName="eui-textTruncate"> - <EndpointPolicyLink - policyId={policy.id} - className="eui-textTruncate" - data-test-subj="policyNameCellLink" - > - {policy.name} - </EndpointPolicyLink> - </EuiToolTip> + <> + <EuiToolTip content={policy.name} anchorClassName="eui-textTruncate"> + <EndpointPolicyLink + policyId={policy.id} + className="eui-textTruncate" + data-test-subj="policyNameCellLink" + > + {policy.name} + </EndpointPolicyLink> + </EuiToolTip> + {isPolicyOutOfDate(policy, item.policy_info) && ( + <OutOfDate style={{ paddingLeft: '6px' }} data-test-subj="rowPolicyOutOfDate" /> + )} + </> ); }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index b4b82b7f692b9..e4e03e9453f7a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -48,7 +48,7 @@ export const AdvancedPolicyForms = React.memo(() => { /> </h4> </EuiText> - <EuiPanel paddingSize="s"> + <EuiPanel data-test-subj="advancedPolicyPanel" paddingSize="s"> {AdvancedPolicySchema.map((advancedField, index) => { const configPath = advancedField.key.split('.'); return ( @@ -114,7 +114,12 @@ const PolicyAdvanced = React.memo( </EuiText> } > - <EuiFieldText fullWidth value={value as string} onChange={onChange} /> + <EuiFieldText + data-test-subj={configPath.join('.')} + fullWidth + value={value as string} + onChange={onChange} + /> </EuiFormRow> </> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 95ad5285507c5..c163ab1ae448b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -7,7 +7,6 @@ import { mount, shallow } from 'enzyme'; import { set } from '@elastic/safer-lodash-set/fp'; import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; import '../../../common/mock/react_beautiful_dnd'; import { @@ -20,10 +19,21 @@ import { } from '../../../common/mock'; import { createStore, State } from '../../../common/store'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; +import * as timelineActions from '../../store/timeline/actions'; -import { Flyout, FlyoutComponent } from '.'; +import { Flyout } from '.'; import { FlyoutButton } from './button'; +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + jest.mock('../timeline', () => ({ // eslint-disable-next-line react/display-name StatefulTimeline: () => <div />, @@ -35,6 +45,10 @@ describe('Flyout', () => { const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); + beforeEach(() => { + mockDispatch.mockClear(); + }); + describe('rendering', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( @@ -162,23 +176,15 @@ describe('Flyout', () => { }); test('should call the onOpen when the mouse is clicked for rendering', () => { - const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; const wrapper = mount( <TestProviders> - <FlyoutComponent - dataProviders={mockDataProviders} - show={false} - showTimeline={showTimeline} - timelineId="test" - width={100} - usersViewing={usersViewing} - /> + <Flyout timelineId="test" usersViewing={usersViewing} /> </TestProviders> ); wrapper.find('[data-test-subj="flyoutOverlay"]').first().simulate('click'); - expect(showTimeline).toBeCalled(); + expect(mockDispatch).toBeCalledWith(timelineActions.showTimeline({ id: 'test', show: true })); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index 7d0f5995afc3b..f5ad6264f95e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -6,17 +6,14 @@ import { EuiBadge } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { State } from '../../../common/store'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions, timelineSelectors } from '../../store/timeline'; -import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; -import { StatefulTimeline } from '../timeline'; -import { TimelineById } from '../../store/timeline/types'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; export const Badge = (styled(EuiBadge)` position: absolute; @@ -40,66 +37,41 @@ interface OwnProps { usersViewing: string[]; } -type Props = OwnProps & ProsFromRedux; - -export const FlyoutComponent = React.memo<Props>( - ({ dataProviders, show = true, showTimeline, timelineId, usersViewing, width }) => { - const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ - showTimeline, - timelineId, - ]); - const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ - showTimeline, - timelineId, - ]); - - return ( - <> - <Visible show={show}> - <Pane onClose={handleClose} timelineId={timelineId} width={width}> - <StatefulTimeline onClose={handleClose} usersViewing={usersViewing} id={timelineId} /> - </Pane> - </Visible> - <FlyoutButton - dataProviders={dataProviders} - show={!show} - timelineId={timelineId} - onOpen={handleOpen} - /> - </> - ); - } -); - -FlyoutComponent.displayName = 'FlyoutComponent'; - const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; const DEFAULT_TIMELINE_BY_ID = {}; -const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById: TimelineById = - timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; - /* - In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender - of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS - */ - const dataProviders = timelineById[timelineId]?.dataProviders.length - ? timelineById[timelineId]?.dataProviders - : DEFAULT_DATA_PROVIDERS; - const show = timelineById[timelineId]?.show ?? false; - const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; - - return { dataProviders, show, width }; -}; - -const mapDispatchToProps = { - showTimeline: timelineActions.showTimeline, +const FlyoutComponent: React.FC<OwnProps> = ({ timelineId, usersViewing }) => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const dispatch = useDispatch(); + const { dataProviders = DEFAULT_DATA_PROVIDERS, show = false } = useDeepEqualSelector( + (state) => getTimeline(state, timelineId) ?? DEFAULT_TIMELINE_BY_ID + ); + const handleClose = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), + [dispatch, timelineId] + ); + const handleOpen = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: true })), + [dispatch, timelineId] + ); + + return ( + <> + <Visible show={show}> + <Pane onClose={handleClose} timelineId={timelineId} usersViewing={usersViewing} /> + </Visible> + <FlyoutButton + dataProviders={dataProviders} + show={!show} + timelineId={timelineId} + onOpen={handleOpen} + /> + </> + ); }; -const connector = connect(mapStateToProps, mapDispatchToProps); - -type ProsFromRedux = ConnectedProps<typeof connector>; +FlyoutComponent.displayName = 'FlyoutComponent'; -export const Flyout = connector(FlyoutComponent); +export const Flyout = React.memo(FlyoutComponent); Flyout.displayName = 'Flyout'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap index f24ef3448d03f..4a314d76a51bf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -4,10 +4,6 @@ exports[`Pane renders correctly against snapshot 1`] = ` <Pane onClose={[MockFunction]} timelineId="test" - width={640} -> - <span> - I am a child of flyout - </span> -</Pane> + usersViewing={Array []} +/> `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx index 3d2c42c33c975..fed6a39ae2ed5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx @@ -4,58 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { Pane } from '.'; -const testWidth = 640; - describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( <TestProviders> - <Pane onClose={jest.fn()} timelineId={'test'} width={testWidth}> - <span>{'I am a child of flyout'}</span> - </Pane> + <Pane onClose={jest.fn()} timelineId={'test'} usersViewing={[]} /> </TestProviders> ); expect(EmptyComponent.find('Pane')).toMatchSnapshot(); }); - - test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { - const wrapper = mount( - <TestProviders> - <Pane onClose={jest.fn()} timelineId={'test'} width={testWidth}> - <span>{'I am a child of flyout'}</span> - </Pane> - </TestProviders> - ); - - expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); - }); - - test('it should render a resize handle', () => { - const wrapper = mount( - <TestProviders> - <Pane onClose={jest.fn()} timelineId={'test'} width={testWidth}> - <span>{'I am a child of flyout'}</span> - </Pane> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="flyout-resize-handle"]').first().exists()).toEqual(true); - }); - - test('it should render children', () => { - const wrapper = mount( - <TestProviders> - <Pane onClose={jest.fn()} timelineId={'test'} width={testWidth}> - <span>{'I am a mock body'}</span> - </Pane> - </TestProviders> - ); - expect(wrapper.first().text()).toContain('I am a mock body'); - }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 7528468ef6522..10eb140515826 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -5,113 +5,48 @@ */ import { EuiFlyout } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; import styled from 'styled-components'; -import { Resizable, ResizeCallback } from 're-resizable'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; -import { useFullScreen } from '../../../../common/containers/use_full_screen'; -import { timelineActions } from '../../../store/timeline'; - -import { TimelineResizeHandle } from './timeline_resize_handle'; - +import { StatefulTimeline } from '../../timeline'; import * as i18n from './translations'; -const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) -const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view interface FlyoutPaneComponentProps { - children: React.ReactNode; onClose: () => void; timelineId: string; - width: number; + usersViewing: string[]; } const EuiFlyoutContainer = styled.div` .timeline-flyout { z-index: 4001; min-width: 150px; - width: auto; + width: 100%; animation: none; } `; -const StyledResizable = styled(Resizable)` - display: flex; - flex-direction: column; -`; - -const RESIZABLE_ENABLE = { left: true }; - -const RESIZABLE_DISABLED = { left: false }; - const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ - children, onClose, timelineId, - width, -}) => { - const dispatch = useDispatch(); - const { timelineFullScreen } = useFullScreen(); - - const onResizeStop: ResizeCallback = useCallback( - (_e, _direction, _ref, delta) => { - const bodyClientWidthPixels = document.body.clientWidth; - - if (delta.width) { - dispatch( - timelineActions.applyDeltaToWidth({ - bodyClientWidthPixels, - delta: -delta.width, - id: timelineId, - maxWidthPercent, - minWidthPixels, - }) - ); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dispatch] - ); - const resizableDefaultSize = useMemo( - () => ({ - width, - height: '100%', - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const resizableHandleComponent = useMemo( - () => ({ - left: <TimelineResizeHandle data-test-subj="flyout-resize-handle" />, - }), - [] - ); - - return ( - <EuiFlyoutContainer data-test-subj="flyout-pane"> - <EuiFlyout - aria-label={i18n.TIMELINE_DESCRIPTION} - className="timeline-flyout" - data-test-subj="eui-flyout" - hideCloseButton={true} - onClose={onClose} - size="l" - > - <StyledResizable - enable={timelineFullScreen ? RESIZABLE_DISABLED : RESIZABLE_ENABLE} - defaultSize={resizableDefaultSize} - minWidth={timelineFullScreen ? 'calc(100vw - 8px)' : minWidthPixels} - maxWidth={timelineFullScreen ? 'calc(100vw - 8px)' : `${maxWidthPercent}vw`} - handleComponent={resizableHandleComponent} - onResizeStop={onResizeStop} - > - <EventDetailsWidthProvider>{children}</EventDetailsWidthProvider> - </StyledResizable> - </EuiFlyout> - </EuiFlyoutContainer> - ); -}; + usersViewing, +}) => ( + <EuiFlyoutContainer data-test-subj="flyout-pane"> + <EuiFlyout + aria-label={i18n.TIMELINE_DESCRIPTION} + className="timeline-flyout" + data-test-subj="eui-flyout" + hideCloseButton={true} + onClose={onClose} + size="l" + > + <EventDetailsWidthProvider> + <StatefulTimeline onClose={onClose} usersViewing={usersViewing} id={timelineId} /> + </EventDetailsWidthProvider> + </EuiFlyout> + </EuiFlyoutContainer> +); export const Pane = React.memo(FlyoutPaneComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx deleted file mode 100644 index 7192580f2426d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import styled from 'styled-components'; - -export const TIMELINE_RESIZE_HANDLE_WIDTH = 4; // px - -export const TimelineResizeHandle = styled.div` - background-color: ${({ theme }) => theme.eui.euiColorLightShade}; - cursor: col-resize; - min-height: 20px; - width: ${TIMELINE_RESIZE_HANDLE_WIDTH}px; - z-index: 2; - height: 100vh; - position: absolute; - &:hover { - background-color: ${({ theme }) => theme.eui.euiColorPrimary}; - } -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 921527a0079e3..20faf93616a8c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -286,6 +286,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -321,7 +322,6 @@ describe('helpers', () => { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }); }); @@ -385,6 +385,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -420,7 +421,6 @@ describe('helpers', () => { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }); }); @@ -484,6 +484,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -519,7 +520,6 @@ describe('helpers', () => { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }); }); @@ -581,6 +581,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -616,7 +617,6 @@ describe('helpers', () => { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }); }); @@ -717,6 +717,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -749,7 +750,6 @@ describe('helpers', () => { sortDirection: 'desc', }, status: TimelineStatus.draft, - width: 1100, id: 'savedObject-1', }); }); @@ -841,6 +841,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [ { $state: { @@ -916,7 +917,6 @@ describe('helpers', () => { sortDirection: 'desc', }, status: TimelineStatus.draft, - width: 1100, id: 'savedObject-1', }); }); @@ -981,6 +981,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -1016,7 +1017,6 @@ describe('helpers', () => { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }); }); @@ -1080,6 +1080,7 @@ describe('helpers', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], + expandedEvent: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -1115,7 +1116,6 @@ describe('helpers', () => { templateTimelineId: null, templateTimelineVersion: null, version: '1', - width: 1100, }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 0afca36309659..a728e35122060 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -28,7 +28,6 @@ describe('Actions', () => { checked={false} expanded={false} eventId="abc" - loading={false} loadingEventIds={[]} onEventToggled={jest.fn()} onRowSelected={jest.fn()} @@ -46,29 +45,8 @@ describe('Actions', () => { <Actions actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} checked={false} - expanded={false} eventId="abc" - loading={false} - loadingEventIds={[]} - onEventToggled={jest.fn()} - onRowSelected={jest.fn()} - showCheckboxes={false} - /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); - }); - - test('it renders a button for expanding the event', () => { - const wrapper = mount( - <TestProviders> - <Actions - actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} - checked={false} expanded={false} - eventId="abc" - loading={false} loadingEventIds={[]} onEventToggled={jest.fn()} onRowSelected={jest.fn()} @@ -77,30 +55,6 @@ describe('Actions', () => { </TestProviders> ); - expect(wrapper.find('[data-test-subj="expand-event"]').exists()).toEqual(true); - }); - - test('it invokes onEventToggled when the button to expand an event is clicked', () => { - const onEventToggled = jest.fn(); - - const wrapper = mount( - <TestProviders> - <Actions - actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} - checked={false} - expanded={false} - eventId="abc" - loading={false} - loadingEventIds={[]} - onEventToggled={onEventToggled} - onRowSelected={jest.fn()} - showCheckboxes={false} - /> - </TestProviders> - ); - - wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); - - expect(onEventToggled).toBeCalled(); + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 3d08d56d6fb19..e942dce724520 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -6,7 +6,7 @@ import React, { useCallback } from 'react'; import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; -import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; +import { EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; import * as i18n from '../translations'; import { OnRowSelected } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; @@ -18,7 +18,6 @@ interface Props { onRowSelected: OnRowSelected; expanded: boolean; eventId: string; - loading: boolean; loadingEventIds: Readonly<string[]>; onEventToggled: () => void; showCheckboxes: boolean; @@ -30,7 +29,6 @@ const ActionsComponent: React.FC<Props> = ({ checked, expanded, eventId, - loading = false, loadingEventIds, onEventToggled, onRowSelected, @@ -68,17 +66,14 @@ const ActionsComponent: React.FC<Props> = ({ )} <EventsTd key="expand-event"> <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}> - {loading ? ( - <EventsLoading /> - ) : ( - <EuiButtonIcon - aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND} - data-test-subj="expand-event" - iconType={expanded ? 'arrowDown' : 'arrowRight'} - id={eventId} - onClick={onEventToggled} - /> - )} + <EuiButtonIcon + aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND} + data-test-subj="expand-event" + disabled={expanded} + iconType="arrowRight" + id={eventId} + onClick={onEventToggled} + /> </EventsTdContent> </EventsTd> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 576dedfc28b1b..6fddb5403561e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -20,5 +20,3 @@ export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ export const DEFAULT_DATE_COLUMN_MIN_WIDTH = 190; // px - -export const DEFAULT_TIMELINE_WIDTH = 1100; // px diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c6d4325f00739..15d7d750257ac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -46,7 +46,6 @@ interface Props { getNotesByIds: (noteIds: string[]) => Note[]; isEventPinned: boolean; isEventViewer?: boolean; - loading: boolean; loadingEventIds: Readonly<string[]>; onColumnResized: OnColumnResized; onEventToggled: () => void; @@ -81,7 +80,6 @@ export const EventColumnView = React.memo<Props>( getNotesByIds, isEventPinned = false, isEventViewer = false, - loading, loadingEventIds, onColumnResized, onEventToggled, @@ -194,7 +192,6 @@ export const EventColumnView = React.memo<Props>( expanded={expanded} data-test-subj="actions" eventId={id} - loading={loading} loadingEventIds={loadingEventIds} onEventToggled={onEventToggled} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index 17dd83e9ab3f4..19d657b0537a5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { inputsModel } from '../../../../../common/store'; -import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; +import { BrowserFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData, @@ -15,13 +15,7 @@ import { import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { Note } from '../../../../../common/lib/note'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { - OnColumnResized, - OnPinEvent, - OnRowSelected, - OnUnPinEvent, - OnUpdateColumns, -} from '../../events'; +import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTbody } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; @@ -34,9 +28,7 @@ interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; - containerElementRef: HTMLDivElement; data: TimelineItem[]; - docValueFields: DocValueFields[]; eventIdToNoteIds: Readonly<Record<string, string[]>>; getNotesByIds: (noteIds: string[]) => Note[]; id: string; @@ -45,7 +37,6 @@ interface Props { onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly<Record<string, boolean>>; refetch: inputsModel.Refetch; @@ -63,9 +54,7 @@ const EventsComponent: React.FC<Props> = ({ browserFields, columnHeaders, columnRenderers, - containerElementRef, data, - docValueFields, eventIdToNoteIds, getNotesByIds, id, @@ -74,7 +63,6 @@ const EventsComponent: React.FC<Props> = ({ onColumnResized, onPinEvent, onRowSelected, - onUpdateColumns, onUnPinEvent, pinnedEventIds, refetch, @@ -82,7 +70,6 @@ const EventsComponent: React.FC<Props> = ({ rowRenderers, selectedEventIds, showCheckboxes, - toggleColumn, updateNote, }) => ( <EventsTbody data-test-subj="events"> @@ -93,8 +80,6 @@ const EventsComponent: React.FC<Props> = ({ browserFields={browserFields} columnHeaders={columnHeaders} columnRenderers={columnRenderers} - containerElementRef={containerElementRef} - docValueFields={docValueFields} event={event} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} @@ -106,14 +91,12 @@ const EventsComponent: React.FC<Props> = ({ onPinEvent={onPinEvent} onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} - onUpdateColumns={onUpdateColumns} refetch={refetch} rowRenderers={rowRenderers} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} timelineId={id} - toggleColumn={toggleColumn} updateNote={updateNote} /> ))} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 83e824aa2450a..6c28c0ce16df1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -4,28 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef, useState, useCallback } from 'react'; +import React, { useMemo, useRef, useState, useCallback } from 'react'; import uuid from 'uuid'; +import { useDispatch } from 'react-redux'; -import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; -import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useTimelineEventsDetails } from '../../../../containers/details'; +import { TimelineId } from '../../../../../../common/types/timeline'; +import { BrowserFields } from '../../../../../common/containers/source'; +import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { - TimelineEventsDetailsItem, TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; import { Note } from '../../../../../common/lib/note'; -import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { - OnColumnResized, - OnPinEvent, - OnRowSelected, - OnUnPinEvent, - OnUpdateColumns, -} from '../../events'; -import { ExpandableEvent } from '../../expandable_event'; +import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; @@ -36,17 +29,15 @@ import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; import { inputsModel } from '../../../../../common/store'; -import { TimelineId } from '../../../../../../common/types/timeline'; +import { timelineActions } from '../../../../store/timeline'; import { activeTimeline } from '../../../../containers/active_timeline_context'; interface Props { actionsColumnWidth: number; - containerElementRef: HTMLDivElement; addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; - docValueFields: DocValueFields[]; event: TimelineItem; eventIdToNoteIds: Readonly<Record<string, string[]>>; getNotesByIds: (noteIds: string[]) => Note[]; @@ -56,7 +47,6 @@ interface Props { onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; - onUpdateColumns: OnUpdateColumns; isEventPinned: boolean; refetch: inputsModel.Refetch; onRuleChange?: () => void; @@ -64,14 +54,11 @@ interface Props { selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; showCheckboxes: boolean; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } export const getNewNoteId = (): string => uuid.v4(); -const emptyDetails: TimelineEventsDetailsItem[] = []; - const emptyNotes: string[] = []; const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { @@ -85,10 +72,8 @@ const StatefulEventComponent: React.FC<Props> = ({ actionsColumnWidth, addNoteToEvent, browserFields, - containerElementRef, columnHeaders, columnRenderers, - docValueFields, event, eventIdToNoteIds, getNotesByIds, @@ -99,43 +84,50 @@ const StatefulEventComponent: React.FC<Props> = ({ onPinEvent, onRowSelected, onUnPinEvent, - onUpdateColumns, refetch, onRuleChange, rowRenderers, selectedEventIds, showCheckboxes, timelineId, - toggleColumn, updateNote, }) => { - const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>( - timelineId === TimelineId.active ? activeTimeline.getExpandedEventIds() : {} - ); + const dispatch = useDispatch(); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const { status: timelineStatus } = useShallowEqualSelector<TimelineModel>( + const { expandedEvent, status: timelineStatus } = useDeepEqualSelector( (state) => state.timeline.timelineById[timelineId] ); const divElement = useRef<HTMLDivElement | null>(null); - const [loading, detailsData] = useTimelineEventsDetails({ - docValueFields, - indexName: event._index!, - eventId: event._id, - skip: !expanded || !expanded[event._id], - }); + + const isExpanded = useMemo(() => expandedEvent && expandedEvent.eventId === event._id, [ + event._id, + expandedEvent, + ]); const onToggleShowNotes = useCallback(() => { const eventId = event._id; setShowNotes((prevShowNotes) => ({ ...prevShowNotes, [eventId]: !prevShowNotes[eventId] })); }, [event]); - const onToggleExpanded = useCallback(() => { + const handleOnEventToggled = useCallback(() => { const eventId = event._id; - setExpanded((prevExpanded) => ({ ...prevExpanded, [eventId]: !prevExpanded[eventId] })); + const indexName = event._index!; + + dispatch( + timelineActions.toggleExpandedEvent({ + timelineId, + event: { + eventId, + indexName, + loading: false, + }, + }) + ); + if (timelineId === TimelineId.active) { - activeTimeline.toggleExpandedEvent(eventId); + activeTimeline.toggleExpandedEvent({ eventId, indexName, loading: false }); } - }, [event._id, timelineId]); + }, [dispatch, event._id, event._index, timelineId]); const associateNote = useCallback( (noteId: string) => { @@ -153,6 +145,7 @@ const StatefulEventComponent: React.FC<Props> = ({ data-test-subj="event" eventType={getEventType(event.ecs)} isBuildingBlockType={isEventBuildingBlockType(event.ecs)} + isExpanded={isExpanded} showLeftBorder={!isEventViewer} ref={divElement} > @@ -164,15 +157,14 @@ const StatefulEventComponent: React.FC<Props> = ({ columnRenderers={columnRenderers} data={event.data} ecsData={event.ecs} - expanded={!!expanded[event._id]} eventIdToNoteIds={eventIdToNoteIds} + expanded={isExpanded} getNotesByIds={getNotesByIds} isEventPinned={isEventPinned} isEventViewer={isEventViewer} - loading={loading} loadingEventIds={loadingEventIds} onColumnResized={onColumnResized} - onEventToggled={onToggleExpanded} + onEventToggled={handleOnEventToggled} onPinEvent={onPinEvent} onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} @@ -209,23 +201,6 @@ const StatefulEventComponent: React.FC<Props> = ({ data: event.ecs, timelineId, })} - - <EventsTrSupplement - className="siemEventsTable__trSupplement--attributes" - data-test-subj="event-details" - > - <ExpandableEvent - browserFields={browserFields} - columnHeaders={columnHeaders} - event={detailsData || emptyDetails} - forceExpand={!!expanded[event._id] && !loading} - id={event._id} - onEventToggled={onToggleExpanded} - onUpdateColumns={onUpdateColumns} - timelineId={timelineId} - toggleColumn={toggleColumn} - /> - </EventsTrSupplement> </EventsTrSupplementContainerWrapper> </EventsTrGroup> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 8fa5d18c0c4f5..99dfd53145e9f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -18,7 +18,6 @@ import { Sort } from './sort'; import { waitFor } from '@testing-library/react'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; -import { TimelineType } from '../../../../../common/types/timeline'; const mockGetNotesByIds = (eventId: string[]) => []; const mockSort: Sort = { @@ -28,6 +27,7 @@ const mockSort: Sort = { jest.mock('../../../../common/hooks/use_selector', () => ({ useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), + useDeepEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), })); jest.mock('../../../../common/components/link_to'); @@ -77,7 +77,6 @@ describe('Body', () => { sort: mockSort, showCheckboxes: false, timelineId: 'timeline-test', - timelineType: TimelineType.default, toggleColumn: jest.fn(), updateNote: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index e1667ab949732..05a66c6853f6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useRef } from 'react'; +import React, { useMemo } from 'react'; import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; import { Note } from '../../../../common/lib/note'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, @@ -29,9 +29,8 @@ import { Events } from './events'; import { ColumnRenderer } from './renderers/column_renderer'; import { RowRenderer } from './renderers/row_renderer'; import { Sort } from './sort'; -import { GraphOverlay } from '../../graph_overlay'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; -import { TimelineEventsType, TimelineId, TimelineType } from '../../../../../common/types/timeline'; +import { TimelineEventsType, TimelineId } from '../../../../../common/types/timeline'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -64,7 +63,6 @@ export interface BodyProps { showCheckboxes: boolean; sort: Sort; timelineId: string; - timelineType: TimelineType; toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } @@ -84,7 +82,6 @@ export const Body = React.memo<BodyProps>( columnHeaders, columnRenderers, data, - docValueFields, eventIdToNoteIds, getNotesByIds, graphEventId, @@ -109,10 +106,8 @@ export const Body = React.memo<BodyProps>( sort, toggleColumn, timelineId, - timelineType, updateNote, }) => { - const containerElementRef = useRef<HTMLDivElement>(null); const actionsColumnWidth = useMemo( () => getActionsColumnWidth( @@ -133,18 +128,9 @@ export const Body = React.memo<BodyProps>( return ( <> - {graphEventId && ( - <GraphOverlay - graphEventId={graphEventId} - isEventViewer={isEventViewer} - timelineId={timelineId} - timelineType={timelineType} - /> - )} <TimelineBody data-test-subj="timeline-body" data-timeline-id={timelineId} - ref={containerElementRef} visible={show && !graphEventId} > <EventsTable data-test-subj="events-table" columnWidths={columnWidths}> @@ -167,14 +153,12 @@ export const Body = React.memo<BodyProps>( /> <Events - containerElementRef={containerElementRef.current!} actionsColumnWidth={actionsColumnWidth} addNoteToEvent={addNoteToEvent} browserFields={browserFields} columnHeaders={columnHeaders} columnRenderers={columnRenderers} data={data} - docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} id={timelineId} @@ -183,7 +167,6 @@ export const Body = React.memo<BodyProps>( onColumnResized={onColumnResized} onPinEvent={onPinEvent} onRowSelected={onRowSelected} - onUpdateColumns={onUpdateColumns} onUnPinEvent={onUnPinEvent} pinnedEventIds={pinnedEventIds} refetch={refetch} @@ -201,4 +184,5 @@ export const Body = React.memo<BodyProps>( ); } ); + Body.displayName = 'Body'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index d7a05e39e76b2..120b3ce165909 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -80,7 +80,6 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>( graphEventId, refetch, sort, - timelineType, toggleColumn, unPinEvent, updateColumns, @@ -220,7 +219,6 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>( showCheckboxes={showCheckboxes} sort={sort} timelineId={id} - timelineType={timelineType} toggleColumn={toggleColumn} updateNote={onUpdateNote} /> @@ -243,8 +241,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>( prevProps.show === nextProps.show && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.sort === nextProps.sort && - prevProps.timelineType === nextProps.timelineType + prevProps.sort === nextProps.sort ); StatefulBodyComponent.displayName = 'StatefulBodyComponent'; @@ -270,7 +267,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - timelineType, } = timeline; return { @@ -286,7 +282,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - timelineType, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx new file mode 100644 index 0000000000000..4b595fad9be6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { BrowserFields, DocValueFields } from '../../../common/containers/source'; +import { + ExpandableEvent, + ExpandableEventTitle, +} from '../../../timelines/components/timeline/expandable_event'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; + +interface EventDetailsProps { + browserFields: BrowserFields; + docValueFields: DocValueFields[]; + timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; +} + +const EventDetailsComponent: React.FC<EventDetailsProps> = ({ + browserFields, + docValueFields, + timelineId, + toggleColumn, +}) => { + const expandedEvent = useDeepEqualSelector( + (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? {} + ); + + return ( + <> + <ExpandableEventTitle /> + <EuiSpacer /> + <ExpandableEvent + browserFields={browserFields} + docValueFields={docValueFields} + event={expandedEvent} + timelineId={timelineId} + toggleColumn={toggleColumn} + /> + </> + ); +}; + +export const EventDetails = React.memo( + EventDetailsComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index b1f48608346c7..77aee2c4bf012 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -4,62 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiTextColor, EuiLoadingContent, EuiTitle } from '@elastic/eui'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; -import { BrowserFields } from '../../../../common/containers/source'; +import { TimelineExpandedEvent } from '../../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; -import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { StatefulEventDetails } from '../../../../common/components/event_details/stateful_event_details'; import { LazyAccordion } from '../../lazy_accordion'; -import { OnUpdateColumns } from '../events'; +import { useTimelineEventsDetails } from '../../../containers/details'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { getColumnHeaders } from '../body/column_headers/helpers'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import * as i18n from './translations'; -const ExpandableDetails = styled.div<{ hideExpandButton: boolean }>` - ${({ hideExpandButton }) => - hideExpandButton - ? ` +const ExpandableDetails = styled.div` .euiAccordion__button { display: none; } - ` - : ''}; `; ExpandableDetails.displayName = 'ExpandableDetails'; interface Props { browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - id: string; - event: TimelineEventsDetailsItem[]; - forceExpand?: boolean; - hideExpandButton?: boolean; - onEventToggled: () => void; - onUpdateColumns: OnUpdateColumns; + docValueFields: DocValueFields[]; + event: TimelineExpandedEvent; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; } +export const ExpandableEventTitle = React.memo(() => ( + <EuiTitle size="s"> + <h4>{i18n.EVENT_DETAILS}</h4> + </EuiTitle> +)); + +ExpandableEventTitle.displayName = 'ExpandableEventTitle'; + export const ExpandableEvent = React.memo<Props>( - ({ - browserFields, - columnHeaders, - event, - forceExpand = false, - id, - timelineId, - toggleColumn, - onEventToggled, - onUpdateColumns, - }) => { + ({ browserFields, docValueFields, event, timelineId, toggleColumn }) => { + const dispatch = useDispatch(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + + const columnHeaders = useDeepEqualSelector((state) => { + const { columns } = getTimeline(state, timelineId) ?? timelineDefaults; + + return getColumnHeaders(columns, browserFields); + }); + + const [loading, detailsData] = useTimelineEventsDetails({ + docValueFields, + indexName: event.indexName!, + eventId: event.eventId!, + skip: !event.eventId, + }); + + const onUpdateColumns = useCallback( + (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })), + [dispatch, timelineId] + ); + const handleRenderExpandedContent = useCallback( () => ( <StatefulEventDetails browserFields={browserFields} columnHeaders={columnHeaders} - data={event} - id={id} - onEventToggled={onEventToggled} + data={detailsData!} + id={event.eventId!} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} @@ -68,21 +83,28 @@ export const ExpandableEvent = React.memo<Props>( [ browserFields, columnHeaders, - event, - id, - onEventToggled, + detailsData, + event.eventId, onUpdateColumns, timelineId, toggleColumn, ] ); + if (!event.eventId) { + return <EuiTextColor color="subdued">{i18n.EVENT_DETAILS_PLACEHOLDER}</EuiTextColor>; + } + + if (loading) { + return <EuiLoadingContent lines={10} />; + } + return ( - <ExpandableDetails hideExpandButton={true}> + <ExpandableDetails> <LazyAccordion - id={`timeline-${timelineId}-row-${id}`} + id={`timeline-${timelineId}-row-${event.eventId}`} renderExpandedContent={handleRenderExpandedContent} - forceExpand={forceExpand} + forceExpand={!!event.eventId && !loading} paddingSize="none" /> </ExpandableDetails> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx index 19b360b24391d..a4c4679c82058 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx @@ -19,3 +19,17 @@ export const EVENT = i18n.translate( defaultMessage: 'Event', } ); + +export const EVENT_DETAILS_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.timeline.expandableEvent.placeholder', + { + defaultMessage: 'Select an event to show its details', + } +); + +export const EVENT_DETAILS = i18n.translate( + 'xpack.securitySolution.timeline.expandableEvent.titleLabel', + { + defaultMessage: 'Event details', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 35d31e034e7f3..baa62b629567d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -18,6 +18,7 @@ import { OnChangeItemsPerPage } from './events'; import { Timeline } from './timeline'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { activeTimeline } from '../../containers/active_timeline_context'; export interface OwnProps { id: string; @@ -98,7 +99,13 @@ const StatefulTimelineComponent = React.memo<Props>( useEffect(() => { if (createTimeline != null && !isTimelineExists) { - createTimeline({ id, columns: defaultHeaders, indexNames: selectedPatterns, show: false }); + createTimeline({ + id, + columns: defaultHeaders, + indexNames: selectedPatterns, + show: false, + expandedEvent: activeTimeline.getExpandedEvent(), + }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -226,7 +233,6 @@ const mapDispatchToProps = { createTimeline: timelineActions.createTimeline, removeColumn: timelineActions.removeColumn, updateColumns: timelineActions.updateColumns, - updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, updateItemsPerPage: timelineActions.updateItemsPerPage, updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, updateSort: timelineActions.updateSort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 5e0d15f3bfbc3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SkeletonRow it renders 1`] = ` -<Row> - <Cell - key="0" - /> - <Cell - key="1" - /> - <Cell - key="2" - /> - <Cell - key="3" - /> -</Row> -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx deleted file mode 100644 index b63359077bf2c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../../common/mock'; -import { SkeletonRow } from './index'; - -describe('SkeletonRow', () => { - test('it renders', () => { - const wrapper = shallow(<SkeletonRow />); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the correct number of cells if cellCount is specified', () => { - const wrapper = mount( - <TestProviders> - <SkeletonRow cellCount={10} /> - </TestProviders> - ); - - expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); - }); - - test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { - const wrapper = mount( - <TestProviders> - <SkeletonRow cellColor="red" cellMargin="10px" rowHeight="100px" rowPadding="10px" /> - </TestProviders> - ); - const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); - const siemSkeletonRowCell = wrapper.find('.siemSkeletonRow__cell').last(); - - expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); - expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); - expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); - expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { - modifier: '& + &', - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx deleted file mode 100644 index ae30f11d8bb16..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx +++ /dev/null @@ -1,77 +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, { useMemo } from 'react'; -import styled from 'styled-components'; - -interface RowProps { - rowHeight?: string; - rowPadding?: string; -} - -const RowComponent = styled.div.attrs<RowProps>(({ rowHeight, rowPadding, theme }) => ({ - className: 'siemSkeletonRow', - rowHeight: rowHeight || theme.eui.euiSizeXL, - rowPadding: rowPadding || `${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.xs}`, -}))<RowProps>` - border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - display: flex; - height: ${({ rowHeight }) => rowHeight}; - padding: ${({ rowPadding }) => rowPadding}; -`; -RowComponent.displayName = 'RowComponent'; - -const Row = React.memo(RowComponent); - -Row.displayName = 'Row'; - -interface CellProps { - cellColor?: string; - cellMargin?: string; -} - -const CellComponent = styled.div.attrs<CellProps>(({ cellColor, cellMargin, theme }) => ({ - className: 'siemSkeletonRow__cell', - cellColor: cellColor || theme.eui.euiColorLightestShade, - cellMargin: cellMargin || theme.eui.gutterTypes.gutterSmall, -}))<CellProps>` - background-color: ${({ cellColor }) => cellColor}; - border-radius: 2px; - flex: 1; - - & + & { - margin-left: ${({ cellMargin }) => cellMargin}; - } -`; -CellComponent.displayName = 'CellComponent'; - -const Cell = React.memo(CellComponent); - -Cell.displayName = 'Cell'; - -export interface SkeletonRowProps extends CellProps, RowProps { - cellCount?: number; -} - -export const SkeletonRow = React.memo<SkeletonRowProps>( - ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding }) => { - const cells = useMemo( - () => - [...Array(cellCount)].map( - (_, i) => <Cell cellColor={cellColor} cellMargin={cellMargin} key={i} />, - [cellCount] - ), - [cellCount, cellColor, cellMargin] - ); - - return ( - <Row rowHeight={rowHeight} rowPadding={rowPadding}> - {cells} - </Row> - ); - } -); -SkeletonRow.displayName = 'SkeletonRow'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index d146818e7ab90..e4c49ce197c2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -176,17 +176,18 @@ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ }))<{ className?: string; eventType: Omit<TimelineEventsType, 'all'>; + isExpanded: boolean; isBuildingBlockType: boolean; showLeftBorder: boolean; }>` border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid ${({ theme }) => theme.eui.euiColorLightShade}; - ${({ theme, eventType, isBuildingBlockType, showLeftBorder }) => + ${({ theme, eventType, showLeftBorder }) => showLeftBorder ? `border-left: 4px solid ${eventType === 'raw' ? theme.eui.euiColorLightShade : theme.eui.euiColorWarning}` : ''}; - ${({ isBuildingBlockType, showLeftBorder }) => + ${({ isBuildingBlockType }) => isBuildingBlockType ? `background: repeating-linear-gradient(127deg, rgba(245, 167, 0, 0.2), rgba(245, 167, 0, 0.2) 1px, rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px);` : ''}; @@ -194,6 +195,16 @@ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ &:hover { background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; } + + ${({ isExpanded, theme }) => + isExpanded && + ` + background: ${theme.eui.euiTableSelectedColor}; + + &:hover { + ${theme.eui.euiTableHoverSelectedColor} + } + `} `; export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 7fc269c954ac4..900699503a3bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -214,19 +214,5 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="timeline-footer"]').exists()).toEqual(true); }); - describe('when there is a graphEventId', () => { - beforeEach(() => { - props.graphEventId = 'graphEventId'; // any string w/ length > 0 works - }); - it('should not show the timeline footer', () => { - const wrapper = mount( - <TestProviders> - <TimelineComponent {...props} /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="timeline-footer"]').exists()).toEqual(false); - }); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index f7c76c110ac3f..d5148eeb3655f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiProgress } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiProgress, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useState, useMemo, useEffect } from 'react'; import styled from 'styled-components'; @@ -35,6 +42,8 @@ import { import { useManageTimeline } from '../manage_timeline'; import { TimelineType, TimelineStatusLiteral } from '../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../detections/components/alerts_table/default_config'; +import { GraphOverlay } from '../graph_overlay'; +import { EventDetails } from './event_details'; const TimelineContainer = styled.div` height: 100%; @@ -79,6 +88,16 @@ const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` padding: 0 10px 5px 12px; `; +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + width: 100%; + overflow: hidden; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + const TimelineTemplateBadge = styled.div` background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; color: #fff; @@ -86,6 +105,12 @@ const TimelineTemplateBadge = styled.div` font-size: 0.8em; `; +const VerticalRule = styled.div` + width: 2px; + height: 100%; + background: ${({ theme }) => theme.eui.euiColorLightShade}; +`; + export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -261,20 +286,30 @@ export const TimelineComponent: React.FC<Props> = ({ loading={loading} refetch={refetch} /> - <StyledEuiFlyoutBody data-test-subj="eui-flyout-body" className="timeline-flyout-body"> - <StatefulBody - browserFields={browserFields} - data={events} - docValueFields={docValueFields} - id={id} - refetch={refetch} - sort={sort} - toggleColumn={toggleColumn} + {graphEventId && ( + <GraphOverlay + graphEventId={graphEventId} + isEventViewer={false} + timelineId={id} + timelineType={timelineType} /> - </StyledEuiFlyoutBody> - { - /** Hide the footer if Resolver is showing. */ - !graphEventId && ( + )} + <FullWidthFlexGroup $visible={!graphEventId}> + <ScrollableFlexItem grow={2}> + <StyledEuiFlyoutBody + data-test-subj="eui-flyout-body" + className="timeline-flyout-body" + > + <StatefulBody + browserFields={browserFields} + data={events} + docValueFields={docValueFields} + id={id} + refetch={refetch} + sort={sort} + toggleColumn={toggleColumn} + /> + </StyledEuiFlyoutBody> <StyledEuiFlyoutFooter data-test-subj="eui-flyout-footer" className="timeline-flyout-footer" @@ -295,8 +330,17 @@ export const TimelineComponent: React.FC<Props> = ({ totalCount={totalCount} /> </StyledEuiFlyoutFooter> - ) - } + </ScrollableFlexItem> + <VerticalRule /> + <ScrollableFlexItem grow={1}> + <EventDetails + browserFields={browserFields} + docValueFields={docValueFields} + timelineId={id} + toggleColumn={toggleColumn} + /> + </ScrollableFlexItem> + </FullWidthFlexGroup> </> ) : null} </TimelineContainer> diff --git a/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts b/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts index 50bf8b37adf28..287fcd7f11e93 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineArgs } from '.'; +import { TimelineExpandedEvent } from '../../../common/types/timeline'; import { TimelineEventsAllRequestOptions } from '../../../common/search_strategy/timeline'; +import { TimelineArgs } from '.'; /* * Future Engineer @@ -17,9 +18,10 @@ import { TimelineEventsAllRequestOptions } from '../../../common/search_strategy * I did not want to put in the store because I was feeling it will feel less temporarily and I did not want other engineer using it * */ + class ActiveTimelineEvents { private _activePage: number = 0; - private _expandedEventIds: Record<string, boolean> = {}; + private _expandedEvent: TimelineExpandedEvent = {}; private _pageName: string = ''; private _request: TimelineEventsAllRequestOptions | null = null; private _response: TimelineArgs | null = null; @@ -32,19 +34,20 @@ class ActiveTimelineEvents { this._activePage = activePage; } - getExpandedEventIds() { - return this._expandedEventIds; + getExpandedEvent() { + return this._expandedEvent; } - toggleExpandedEvent(eventId: string) { - this._expandedEventIds = { - ...this._expandedEventIds, - [eventId]: !this._expandedEventIds[eventId], - }; + toggleExpandedEvent(expandedEvent: TimelineExpandedEvent) { + if (expandedEvent.eventId === this._expandedEvent.eventId) { + this._expandedEvent = {}; + } else { + this._expandedEvent = expandedEvent; + } } - setExpandedEventIds(expandedEventIds: Record<string, boolean>) { - this._expandedEventIds = expandedEventIds; + setExpandedEvent(expandedEvent: TimelineExpandedEvent) { + this._expandedEvent = expandedEvent; } getPageName() { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 5f92596f03311..2465d0a536482 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -136,7 +136,7 @@ export const useTimelineEvents = ({ clearSignalsState(); if (id === TimelineId.active) { - activeTimeline.setExpandedEventIds({}); + activeTimeline.setExpandedEvent({}); activeTimeline.setActivePage(newActivePage); } @@ -200,7 +200,7 @@ export const useTimelineEvents = ({ updatedAt: Date.now(), }; if (id === TimelineId.active) { - activeTimeline.setExpandedEventIds({}); + activeTimeline.setExpandedEvent({}); activeTimeline.setPageName(pageName); activeTimeline.setRequest(request); activeTimeline.setResponse(newTimelineResponse); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index c066de8af9f20..c2fff49afdcbf 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -19,6 +19,7 @@ import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { TimelineEventsType, + TimelineExpandedEvent, TimelineTypeLiteral, RowRendererId, } from '../../../../common/types/timeline'; @@ -34,6 +35,12 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI 'ADD_NOTE_TO_EVENT' ); +interface ToggleExpandedEvent { + timelineId: string; + event: TimelineExpandedEvent; +} +export const toggleExpandedEvent = actionCreator<ToggleExpandedEvent>('TOGGLE_EXPANDED_EVENT'); + export const upsertColumn = actionCreator<{ column: ColumnHeaderOptions; id: string; @@ -42,14 +49,6 @@ export const upsertColumn = actionCreator<{ export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); -export const applyDeltaToWidth = actionCreator<{ - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; -}>('APPLY_DELTA_TO_WIDTH'); - export const applyDeltaToColumnWidth = actionCreator<{ id: string; columnId: string; @@ -64,6 +63,7 @@ export interface TimelineInput { end: string; }; excludedRowRendererIds?: RowRendererId[]; + expandedEvent?: TimelineExpandedEvent; filters?: Filter[]; columns: ColumnHeaderOptions[]; itemsPerPage?: number; @@ -173,11 +173,6 @@ export const updateDataProviderType = actionCreator<{ providerId: string; }>('UPDATE_PROVIDER_TYPE'); -export const updateHighlightedDropAndProviderId = actionCreator<{ - id: string; - providerId: string; -}>('UPDATE_DROP_AND_PROVIDER'); - export const updateDescription = actionCreator<{ id: string; description: string; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index ce469c2bf57a2..39174c9092af5 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -7,7 +7,6 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { Direction } from '../../../graphql/types'; -import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { SubsetTimelineModel, TimelineModel } from './model'; @@ -24,6 +23,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter eventType: 'all', eventIdToNoteIds: {}, excludedRowRendererIds: [], + expandedEvent: {}, highlightedDropAndProviderId: '', historyIds: [], filters: [], @@ -57,6 +57,5 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter sortDirection: Direction.desc, }, status: TimelineStatus.draft, - width: DEFAULT_TIMELINE_WIDTH, version: null, }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index 92a913c9c3375..78e30bd81817c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -89,6 +89,7 @@ describe('Epic Timeline', () => { description: '', eventIdToNoteIds: {}, eventType: 'all', + expandedEvent: {}, excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], @@ -150,7 +151,6 @@ describe('Epic Timeline', () => { showCheckboxes: false, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, status: TimelineStatus.active, - width: 1100, version: 'WzM4LDFd', id: '11169110-fc22-11e9-8ca9-072f15ce2685', savedQueryId: 'my endgame timeline query', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 9a0bf5ec4a940..241b8c5030de7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -23,6 +23,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/m import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { TimelineEventsType, + TimelineExpandedEvent, TimelineTypeLiteral, TimelineType, RowRendererId, @@ -142,7 +143,7 @@ export const addTimelineToStore = ({ }: AddTimelineParams): TimelineById => { if (shouldResetActiveTimelineContext(id, timelineById[id], timeline)) { activeTimeline.setActivePage(0); - activeTimeline.setExpandedEventIds({}); + activeTimeline.setExpandedEvent({}); } return { ...timelineById, @@ -169,6 +170,7 @@ interface AddNewTimelineParams { end: string; }; excludedRowRendererIds?: RowRendererId[]; + expandedEvent?: TimelineExpandedEvent; filters?: Filter[]; id: string; itemsPerPage?: number; @@ -190,6 +192,7 @@ export const addNewTimeline = ({ dataProviders = [], dateRange: maybeDateRange, excludedRowRendererIds = [], + expandedEvent = {}, filters = timelineDefaults.filters, id, itemsPerPage = timelineDefaults.itemsPerPage, @@ -218,6 +221,7 @@ export const addNewTimeline = ({ columns, dataProviders, dateRange, + expandedEvent, excludedRowRendererIds, filters, itemsPerPage, @@ -303,39 +307,6 @@ export const updateGraphEventId = ({ }; }; -interface ApplyDeltaToCurrentWidthParams { - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; - timelineById: TimelineById; -} - -export const applyDeltaToCurrentWidth = ({ - id, - delta, - bodyClientWidthPixels, - minWidthPixels, - maxWidthPercent, - timelineById, -}: ApplyDeltaToCurrentWidthParams): TimelineById => { - const timeline = timelineById[id]; - - const requestedWidth = timeline.width + delta * -1; // raw change in width - const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; - const clampedWidth = Math.min(requestedWidth, maxWidthPixels); - const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min - - return { - ...timelineById, - [id]: { - ...timeline, - width, - }, - }; -}; - const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { return true; 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 ec4d37d3b70a2..7d015c1dc82b1 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 @@ -13,6 +13,7 @@ import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline' import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; import type { TimelineEventsType, + TimelineExpandedEvent, TimelineType, TimelineStatus, RowRendererId, @@ -57,6 +58,7 @@ export interface TimelineModel { eventIdToNoteIds: Record<string, string[]>; /** A list of Ids of excluded Row Renderers */ excludedRowRendererIds: RowRendererId[]; + expandedEvent: TimelineExpandedEvent; filters?: Filter[]; /** When non-empty, display a graph view for this event */ graphEventId?: string; @@ -117,8 +119,6 @@ export interface TimelineModel { sort: Sort; /** status: active | draft */ status: TimelineStatus; - /** Persists the UI state (width) of the timeline flyover */ - width: number; /** timeline is saving */ isSaving: boolean; isLoading: boolean; @@ -135,6 +135,7 @@ export type SubsetTimelineModel = Readonly< | 'eventType' | 'eventIdToNoteIds' | 'excludedRowRendererIds' + | 'expandedEvent' | 'graphEventId' | 'highlightedDropAndProviderId' | 'historyIds' @@ -159,7 +160,6 @@ export type SubsetTimelineModel = Readonly< | 'show' | 'showCheckboxes' | 'sort' - | 'width' | 'isSaving' | 'isLoading' | 'savedObjectId' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 7bd86cd7e2452..cd89c9df7e3db 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -14,10 +14,7 @@ import { DataProvidersAnd, } from '../../../timelines/components/timeline/data_providers/data_provider'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_TIMELINE_WIDTH, -} from '../../../timelines/components/timeline/body/constants'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import { getColumnWidthFromType } from '../../../timelines/components/timeline/body/column_headers/helpers'; import { Direction } from '../../../graphql/types'; import { defaultHeaders } from '../../../common/mock'; @@ -81,6 +78,7 @@ const basicTimeline: TimelineModel = { description: '', eventIdToNoteIds: {}, excludedRowRendererIds: [], + expandedEvent: {}, highlightedDropAndProviderId: '', historyIds: [], id: 'foo', @@ -112,7 +110,6 @@ const basicTimeline: TimelineModel = { timelineType: TimelineType.default, title: '', version: null, - width: DEFAULT_TIMELINE_WIDTH, }; const timelineByIdMock: TimelineById = { foo: { ...basicTimeline }, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 7c227f1c80610..3f2b56b3f7dba 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -12,7 +12,6 @@ import { addProvider, addTimeline, applyDeltaToColumnWidth, - applyDeltaToWidth, applyKqlFilterQuery, clearEventsDeleted, clearEventsLoading, @@ -34,6 +33,7 @@ import { showCallOutUnauthorizedMsg, showTimeline, startTimelineSaving, + toggleExpandedEvent, unPinEvent, updateAutoSaveMsg, updateColumns, @@ -43,7 +43,6 @@ import { updateDataProviderType, updateDescription, updateEventType, - updateHighlightedDropAndProviderId, updateIndexNames, updateIsFavorite, updateIsLive, @@ -67,7 +66,6 @@ import { addTimelineNoteToEvent, addTimelineProvider, addTimelineToStore, - applyDeltaToCurrentWidth, applyDeltaToTimelineColumnWidth, applyKqlFilterQueryDraft, pinTimelineEvent, @@ -78,7 +76,6 @@ import { setSelectedTimelineEvents, unPinTimelineEvent, updateExcludedRowRenderersIds, - updateHighlightedDropAndProvider, updateKqlFilterQueryDraft, updateTimelineColumns, updateTimelineDescription, @@ -181,6 +178,16 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }), })) + .case(toggleExpandedEvent, (state, { timelineId, event }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [timelineId]: { + ...state.timelineById[timelineId], + expandedEvent: event, + }, + }, + })) .case(addProvider, (state, { id, provider }) => ({ ...state, timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }), @@ -218,20 +225,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case( - applyDeltaToWidth, - (state, { id, delta, bodyClientWidthPixels, minWidthPixels, maxWidthPercent }) => ({ - ...state, - timelineById: applyDeltaToCurrentWidth({ - id, - delta, - bodyClientWidthPixels, - minWidthPixels, - maxWidthPercent, - timelineById: state.timelineById, - }), - }) - ) .case(pinEvent, (state, { id, eventId }) => ({ ...state, timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }), @@ -485,14 +478,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(updateHighlightedDropAndProviderId, (state, { id, providerId }) => ({ - ...state, - timelineById: updateHighlightedDropAndProvider({ - id, - providerId, - timelineById: state.timelineById, - }), - })) .case(updateAutoSaveMsg, (state, { timelineId, newTimelineModel }) => ({ ...state, autoSavedWarningMsg: { diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 11964ab4d7b28..58e2ea6111a38 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -10,7 +10,13 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; -import { AgentService, FleetStartContract, PackageService } from '../../../fleet/server'; +import { + AgentService, + FleetStartContract, + PackageService, + AgentPolicyServiceInterface, + PackagePolicyServiceInterface, +} from '../../../fleet/server'; import { PluginStartContract as AlertsPluginStartContract } from '../../../alerts/server'; import { getPackagePolicyCreateCallback } from './ingest_integration'; import { ManifestManager } from './services/artifacts'; @@ -66,7 +72,10 @@ export const createMetadataService = (packageService: PackageService): MetadataS }; export type EndpointAppContextServiceStartContract = Partial< - Pick<FleetStartContract, 'agentService' | 'packageService'> + Pick< + FleetStartContract, + 'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService' + > > & { logger: Logger; manifestManager?: ManifestManager; @@ -85,11 +94,15 @@ export type EndpointAppContextServiceStartContract = Partial< export class EndpointAppContextService { private agentService: AgentService | undefined; private manifestManager: ManifestManager | undefined; + private packagePolicyService: PackagePolicyServiceInterface | undefined; + private agentPolicyService: AgentPolicyServiceInterface | undefined; private savedObjectsStart: SavedObjectsServiceStart | undefined; private metadataService: MetadataService | undefined; public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; + this.packagePolicyService = dependencies.packagePolicyService; + this.agentPolicyService = dependencies.agentPolicyService; this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; this.metadataService = createMetadataService(dependencies.packageService!); @@ -115,6 +128,14 @@ export class EndpointAppContextService { return this.agentService; } + public getPackagePolicyService(): PackagePolicyServiceInterface | undefined { + return this.packagePolicyService; + } + + public getAgentPolicyService(): AgentPolicyServiceInterface | undefined { + return this.agentPolicyService; + } + public getMetadataService(): MetadataService | undefined { return this.metadataService; } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 7a1a0f06a2267..1268c8a4bc576 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -9,13 +9,12 @@ import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mock import { securityMock } from '../../../security/server/mocks'; import { alertsMock } from '../../../alerts/server/mocks'; import { xpackMocks } from '../../../../mocks'; +import { FleetStartContract, ExternalCallback, PackageService } from '../../../fleet/server'; import { - AgentService, - FleetStartContract, - ExternalCallback, - PackageService, -} from '../../../fleet/server'; -import { createPackagePolicyServiceMock } from '../../../fleet/server/mocks'; + createPackagePolicyServiceMock, + createMockAgentPolicyService, + createMockAgentService, +} from '../../../fleet/server/mocks'; import { AppClientFactory } from '../client'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { @@ -25,6 +24,7 @@ import { import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager'; import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { EndpointAppContext } from './types'; +import { MetadataRequestContext } from './routes/metadata/handlers'; /** * Creates a mocked EndpointAppContext. @@ -49,6 +49,7 @@ export const createMockEndpointAppContextService = ( start: jest.fn(), stop: jest.fn(), getAgentService: jest.fn(), + getAgentPolicyService: jest.fn(), getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()), getScopedSavedObjectsClient: jest.fn(), } as unknown) as jest.Mocked<EndpointAppContextService>; @@ -90,18 +91,6 @@ export const createMockPackageService = (): jest.Mocked<PackageService> => { }; }; -/** - * Creates a mock AgentService - */ -export const createMockAgentService = (): jest.Mocked<AgentService> => { - return { - getAgentStatusById: jest.fn(), - authenticateAgentWithAccessToken: jest.fn(), - getAgent: jest.fn(), - listAgents: jest.fn(), - }; -}; - /** * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's * ESIndexPatternService. @@ -116,11 +105,20 @@ export const createMockFleetStartContract = (indexPattern: string): FleetStartCo }, agentService: createMockAgentService(), packageService: createMockPackageService(), + agentPolicyService: createMockAgentPolicyService(), registerExternalCallback: jest.fn((...args: ExternalCallback) => {}), packagePolicyService: createPackagePolicyServiceMock(), }; }; +export const createMockMetadataRequestContext = (): jest.Mocked<MetadataRequestContext> => { + return { + endpointAppContextService: createMockEndpointAppContextService(), + logger: loggingSystemMock.create().get('mock_endpoint_app_context'), + requestHandlerContext: xpackMocks.createRequestHandlerContext(), + }; +}; + export function createRouteHandlerContext( dataClient: jest.Mocked<ILegacyScopedClusterClient>, savedObjectsClient: jest.Mocked<SavedObjectsClientContract> diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts new file mode 100644 index 0000000000000..5dd668b857229 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types'; +import { createMockMetadataRequestContext } from '../../mocks'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { enrichHostMetadata, MetadataRequestContext } from './handlers'; + +describe('test document enrichment', () => { + let metaReqCtx: jest.Mocked<MetadataRequestContext>; + const docGen = new EndpointDocGenerator(); + + beforeEach(() => { + metaReqCtx = createMockMetadataRequestContext(); + }); + + // verify query version passed through + describe('metadata query strategy enrichment', () => { + it('should match v1 strategy when directed', async () => { + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_1 + ); + expect(enrichedHostList.query_strategy_version).toEqual( + MetadataQueryStrategyVersions.VERSION_1 + ); + }); + it('should match v2 strategy when directed', async () => { + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.query_strategy_version).toEqual( + MetadataQueryStrategyVersions.VERSION_2 + ); + }); + }); + + describe('host status enrichment', () => { + let statusFn: jest.Mock; + + beforeEach(() => { + statusFn = jest.fn(); + (metaReqCtx.endpointAppContextService.getAgentService as jest.Mock).mockImplementation(() => { + return { + getAgentStatusById: statusFn, + }; + }); + }); + + it('should return host online for online agent', async () => { + statusFn.mockImplementation(() => 'online'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.ONLINE); + }); + + it('should return host offline for offline agent', async () => { + statusFn.mockImplementation(() => 'offline'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.OFFLINE); + }); + + it('should return host unenrolling for unenrolling agent', async () => { + statusFn.mockImplementation(() => 'unenrolling'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.UNENROLLING); + }); + + it('should return host error for degraded agent', async () => { + statusFn.mockImplementation(() => 'degraded'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR); + }); + + it('should return host error for erroring agent', async () => { + statusFn.mockImplementation(() => 'error'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR); + }); + + it('should return host error for warning agent', async () => { + statusFn.mockImplementation(() => 'warning'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR); + }); + + it('should return host error for invalid agent', async () => { + statusFn.mockImplementation(() => 'asliduasofb'); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR); + }); + }); + + describe('policy info enrichment', () => { + let agentMock: jest.Mock; + let agentPolicyMock: jest.Mock; + + beforeEach(() => { + agentMock = jest.fn(); + agentPolicyMock = jest.fn(); + (metaReqCtx.endpointAppContextService.getAgentService as jest.Mock).mockImplementation(() => { + return { + getAgent: agentMock, + getAgentStatusById: jest.fn(), + }; + }); + (metaReqCtx.endpointAppContextService.getAgentPolicyService as jest.Mock).mockImplementation( + () => { + return { + get: agentPolicyMock, + }; + } + ); + }); + + it('reflects current applied agent info', async () => { + const policyID = 'abc123'; + const policyRev = 9; + agentMock.mockImplementation(() => { + return { + policy_id: policyID, + policy_revision: policyRev, + }; + }); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.policy_info).toBeDefined(); + expect(enrichedHostList.policy_info!.agent.applied.id).toEqual(policyID); + expect(enrichedHostList.policy_info!.agent.applied.revision).toEqual(policyRev); + }); + + it('reflects current fleet agent info', async () => { + const policyID = 'xyz456'; + const policyRev = 15; + agentPolicyMock.mockImplementation(() => { + return { + id: policyID, + revision: policyRev, + }; + }); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.policy_info).toBeDefined(); + expect(enrichedHostList.policy_info!.agent.configured.id).toEqual(policyID); + expect(enrichedHostList.policy_info!.agent.configured.revision).toEqual(policyRev); + }); + + it('reflects current endpoint policy info', async () => { + const policyID = 'endpoint-b33f'; + const policyRev = 2; + agentPolicyMock.mockImplementation(() => { + return { + package_policies: [ + { + package: { name: 'endpoint' }, + id: policyID, + revision: policyRev, + }, + ], + }; + }); + + const enrichedHostList = await enrichHostMetadata( + docGen.generateHostMetadata(), + metaReqCtx, + MetadataQueryStrategyVersions.VERSION_2 + ); + expect(enrichedHostList.policy_info).toBeDefined(); + expect(enrichedHostList.policy_info!.endpoint.id).toEqual(policyID); + expect(enrichedHostList.policy_info!.endpoint.revision).toEqual(policyRev); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index f2011e99565c8..a79175b178c38 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -15,7 +15,7 @@ import { MetadataQueryStrategyVersions, } from '../../../../common/endpoint/types'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; -import { Agent, AgentStatus } from '../../../../../fleet/common/types/models'; +import { Agent, AgentStatus, PackagePolicy } from '../../../../../fleet/common/types/models'; import { EndpointAppContext, HostListQueryResult } from '../../types'; import { GetMetadataListRequestSchema, GetMetadataRequestSchema } from './index'; import { findAllUnenrolledAgentIds } from './support/unenroll'; @@ -245,7 +245,7 @@ export async function mapToHostResultList( } } -async function enrichHostMetadata( +export async function enrichHostMetadata( hostMetadata: HostMetadata, metadataRequestContext: MetadataRequestContext, metadataQueryStrategyVersion: MetadataQueryStrategyVersions @@ -282,9 +282,53 @@ async function enrichHostMetadata( throw e; } } + + let policyInfo: HostInfo['policy_info']; + try { + const agent = await metadataRequestContext.endpointAppContextService + ?.getAgentService() + ?.getAgent( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + elasticAgentId + ); + const agentPolicy = await metadataRequestContext.endpointAppContextService + .getAgentPolicyService() + ?.get( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + agent?.policy_id!, + true + ); + const endpointPolicy = ((agentPolicy?.package_policies || []) as PackagePolicy[]).find( + (policy: PackagePolicy) => policy.package?.name === 'endpoint' + ); + + policyInfo = { + agent: { + applied: { + revision: agent?.policy_revision || 0, + id: agent?.policy_id || '', + }, + configured: { + revision: agentPolicy?.revision || 0, + id: agentPolicy?.id || '', + }, + }, + endpoint: { + revision: endpointPolicy?.revision || 0, + id: endpointPolicy?.id || '', + }, + }; + } catch (e) { + // this is a non-vital enrichment of expected policy revisions. + // if we fail just fetching these, the rest of the endpoint + // data should still be returned. log the error and move on + log.error(e); + } + return { metadata: hostMetadata, host_status: hostStatus, + policy_info: policyInfo, query_strategy_version: metadataQueryStrategyVersion, }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts index ed3c48ed6c677..e9a1f1e24fa55 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { findAgentIDsByStatus } from './agent_status'; import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; import { AgentService } from '../../../../../../fleet/server/services'; -import { createMockAgentService } from '../../../mocks'; +import { createMockAgentService } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts index cd273f785033c..c88f11422d0f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { findAllUnenrolledAgentIds } from './unenroll'; import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; import { AgentService } from '../../../../../../fleet/server/services'; -import { createMockAgentService } from '../../../mocks'; +import { createMockAgentService } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; describe('test find all unenrolled Agent id', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 009ce043db85e..0fc3f5135c8f6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -5,10 +5,10 @@ */ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { - createMockAgentService, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; +import { createMockAgentService } from '../../../../../fleet/server/mocks'; import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers'; import { ILegacyScopedClusterClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index a704d076880bf..e50956e9ef752 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -103,6 +103,14 @@ export const buildSignalGroupFromSequence = ( outputIndex ); + if ( + wrappedBuildingBlocks.some((block) => + block._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleSO.id) + ) + ) { + return []; + } + // Now that we have an array of building blocks for the events in the sequence, // we can build the signal that links the building blocks together // and also insert the group id (which is also the "shell" signal _id) in each building block diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 4eda9150e52f1..003626e319007 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -58,7 +58,7 @@ import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects import { getNotificationResultsLink } from '../notifications/utils'; import { TelemetryEventsSender } from '../../telemetry/sender'; import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_query_filter'; -import { bulkInsertSignals } from './single_bulk_create'; +import { bulkInsertSignals, filterDuplicateSignals } from './single_bulk_create'; import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk_body'; import { createThreatSignals } from './threat_mapping/create_threat_signals'; import { getIndexVersion } from '../routes/index/get_index_version'; @@ -495,16 +495,17 @@ export const signalRulesAlertType = ({ [] ); } else if (response.hits.events !== undefined) { - newSignals = response.hits.events.map((event) => - wrapSignal(buildSignalFromEvent(event, savedObject, true), outputIndex) + newSignals = filterDuplicateSignals( + savedObject.id, + response.hits.events.map((event) => + wrapSignal(buildSignalFromEvent(event, savedObject, true), outputIndex) + ) ); } else { throw new Error( 'eql query response should have either `sequences` or `events` but had neither' ); } - // TODO: replace with code that filters out recursive rule signals while allowing sequences and their building blocks - // const filteredSignals = filterDuplicateSignals(alertId, newSignals); if (newSignals.length > 0) { const insertResult = await bulkInsertSignals(newSignals, logger, services, refresh); result.bulkCreateTimes.push(insertResult.bulkCreateDuration); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index d8889dcfcf471..8c1d4210a7b36 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -7,7 +7,7 @@ import { countBy, isEmpty } from 'lodash'; import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../alerts/server'; -import { SignalSearchResponse, BulkResponse, SignalHit, BaseSignalHit } from './types'; +import { SignalSearchResponse, BulkResponse, BaseSignalHit } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString, errorAggregator } from './utils'; @@ -68,9 +68,9 @@ export const filterDuplicateRules = ( * @param ruleId The rule id * @param signals The candidate new signals */ -export const filterDuplicateSignals = (ruleId: string, signals: SignalHit[]) => { +export const filterDuplicateSignals = (ruleId: string, signals: BaseSignalHit[]) => { return signals.filter( - (doc) => !doc.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) + (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId) ); }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 8a33b1df4caa8..d963b3b093d81 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -347,6 +347,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S this.endpointAppContextService.start({ agentService: plugins.fleet?.agentService, packageService: plugins.fleet?.packageService, + packagePolicyService: plugins.fleet?.packagePolicyService, + agentPolicyService: plugins.fleet?.agentPolicyService, appClientFactory: this.appClientFactory, security: this.setupPlugins!.security!, alerts: plugins.alerts, diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx index e5e43210d1e6b..0a722734ffc5a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx @@ -8,7 +8,11 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; +import { + IErrorObject, + AlertsContextValue, + AlertTypeParamsExpressionProps, +} from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_FIELD_TYPES } from '../../types'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; @@ -23,7 +27,7 @@ interface Props { errors: IErrorObject; setAlertParamsDate: (date: string) => void; setAlertParamsGeoField: (geoField: string) => void; - setAlertProperty: (alertProp: string, alertParams: unknown) => void; + setAlertProperty: AlertTypeParamsExpressionProps['setAlertProperty']; setIndexPattern: (indexPattern: IIndexPattern) => void; indexPattern: IIndexPattern; isInvalid: boolean; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index c2e62b6e1898b..3470ee4d76486 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -477,6 +477,41 @@ describe('Workload Statistics Aggregator', () => { }, reject); }); }); + + test('recovery after errors occurrs at the next interval', async () => { + const refreshInterval = 1000; + + const taskStore = taskStoreMock.create({}); + const logger = loggingSystemMock.create().get(); + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + refreshInterval, + 3000, + logger + ); + + return new Promise((resolve, reject) => { + let errorWasThrowAt = 0; + taskStore.aggregate.mockImplementation(async () => { + if (errorWasThrowAt === 0) { + errorWasThrowAt = Date.now(); + throw new Error(`Elasticsearch has gone poof`); + } else if (Date.now() - errorWasThrowAt < refreshInterval) { + reject(new Error(`Elasticsearch is still poof`)); + } + + return setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { + idle: 2, + }); + }); + + workloadAggregator.pipe(take(2), bufferCount(2)).subscribe((results) => { + expect(results.length).toEqual(2); + resolve(); + }, reject); + }); + }); }); describe('estimateRecurringTaskScheduling', () => { diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index a27b5e2282e32..8002ee44d01ff 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -5,7 +5,7 @@ */ import { combineLatest, Observable, timer } from 'rxjs'; -import { mergeMap, map, filter, catchError } from 'rxjs/operators'; +import { mergeMap, map, filter, switchMap, catchError } from 'rxjs/operators'; import { Logger } from 'src/core/server'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { keyBy, mapValues } from 'lodash'; @@ -222,8 +222,8 @@ export function createWorkloadAggregator( }), catchError((ex: Error, caught) => { logger.error(`[WorkloadAggregator]: ${ex}`); - // continue to pull values from the same observable - return caught; + // continue to pull values from the same observable but only on the next refreshInterval + return timer(refreshInterval).pipe(switchMap(() => caught)); }) ); } diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index c965623ebfc17..8936cdafa3827 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1778,30 +1778,6 @@ } } }, - "infraops": { - "properties": { - "last_24_hours": { - "properties": { - "hits": { - "properties": { - "infraops_hosts": { - "type": "long" - }, - "infraops_docker": { - "type": "long" - }, - "infraops_kubernetes": { - "type": "long" - }, - "logs": { - "type": "long" - } - } - } - } - } - } - }, "ingest_manager": { "properties": { "fleet_enabled": { @@ -1841,6 +1817,30 @@ } } }, + "infraops": { + "properties": { + "last_24_hours": { + "properties": { + "hits": { + "properties": { + "infraops_hosts": { + "type": "long" + }, + "infraops_docker": { + "type": "long" + }, + "infraops_kubernetes": { + "type": "long" + }, + "logs": { + "type": "long" + } + } + } + } + } + } + }, "lens": { "properties": { "events_30_days": { @@ -3136,6 +3136,50 @@ } } }, + "saved_objects_tagging": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + }, + "types": { + "properties": { + "dashboard": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + }, + "visualization": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + } + } + } + } + }, "security_solution": { "properties": { "detections": { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index beb6325e4fecd..04b0ef045fffe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -240,7 +240,6 @@ "apmOss.tutorial.startServer.title": "APM Server の起動", "apmOss.tutorial.windowsServerInstructions.textPost": "注:システムでスクリプトの実行が無効な場合、スクリプトを実行するために現在のセッションの実行ポリシーの設定が必要となります。例: {command}。", "apmOss.tutorial.windowsServerInstructions.textPre": "1.[ダウンロードページ]({downloadPageLink}) から APM Server Windows zip ファイルをダウンロードします。\n2.zip ファイルの内容を {zipFileExtractFolder} に抽出します。\n3.「{apmServerDirectory} ディレクトリの名前を「APM-Server」に変更します。\n4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n5.PowerShellプロンプトで次のコマンドを実行し、APM ServerをWindowsサービスとしてインストールします。", - "charts.advancedSettings.visualization.colorMappingText": "ビジュアライゼーション内の特定の色のマップ値です", "charts.advancedSettings.visualization.colorMappingTitle": "カラーマッピング", "charts.colormaps.bluesText": "青", "charts.colormaps.greensText": "緑", @@ -4961,7 +4960,6 @@ "xpack.apm.metadataTable.section.urlLabel": "URL", "xpack.apm.metadataTable.section.userAgentLabel": "ユーザーエージェント", "xpack.apm.metadataTable.section.userLabel": "ユーザー", - "xpack.apm.metrics.plot.noDataLabel": "この時間範囲のデータがありません。", "xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "平均期間の周りのストリームには予測バウンドが表示されます。異常スコアが>= 75の場合、注釈が表示されます。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません", @@ -5079,7 +5077,6 @@ "xpack.apm.servicesTable.transactionsPerMinuteColumnLabel": "1 分あたりのトランザクション", "xpack.apm.servicesTable.transactionsPerMinuteUnitLabel": "1分あたりトランザクション数", "xpack.apm.servicesTable.UpgradeAssistantLink": "Kibana アップグレードアシスタントで詳細をご覧ください", - "xpack.apm.serviceVersion": "サービスバージョン", "xpack.apm.settings.agentConfig": "エージェントの編集", "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "以前の統合のレガシー機械学習ジョブが見つかりました。これは、APMアプリでは使用されていません。", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "ジョブの確認", @@ -5185,9 +5182,7 @@ "xpack.apm.transactionActionMenu.actionsButtonLabel": "アクション", "xpack.apm.transactionActionMenu.container.subtitle": "このコンテナーのログとインデックスを表示し、さらに詳細を確認できます。", "xpack.apm.transactionActionMenu.container.title": "コンテナーの詳細", - "xpack.apm.transactionActionMenu.customLink.popover.title": "カスタムリンク", "xpack.apm.transactionActionMenu.customLink.section": "カスタムリンク", - "xpack.apm.transactionActionMenu.customLink.seeMore": "詳細を表示", "xpack.apm.transactionActionMenu.customLink.subtitle": "リンクは新しいウィンドウで開きます。", "xpack.apm.transactionActionMenu.host.subtitle": "ホストログとメトリックを表示し、さらに詳細を確認できます。", "xpack.apm.transactionActionMenu.host.title": "ホストの詳細", @@ -5287,7 +5282,6 @@ "xpack.apm.ux.percentile.label": "パーセンタイル", "xpack.apm.ux.title": "ユーザーエクスペリエンス", "xpack.apm.ux.visitorBreakdown.noData": "データがありません。", - "xpack.apm.version": "バージョン", "xpack.apm.waterfall.exceedsMax": "このトレースの項目数は表示されている範囲を超えています", "xpack.beatsManagement.beat.actionSectionTypeLabel": "タイプ: {beatType}。", "xpack.beatsManagement.beat.actionSectionVersionLabel": "バージョン: {beatVersion}", @@ -7176,17 +7170,13 @@ "xpack.fleet.agentDetails.agentVersionLabel": "エージェントバージョン", "xpack.fleet.agentDetails.hostIdLabel": "エージェントID", "xpack.fleet.agentDetails.hostNameLabel": "ホスト名", - "xpack.fleet.agentDetails.localMetadataSectionSubtitle": "メタデータを読み込み中", - "xpack.fleet.agentDetails.metadataSectionTitle": "メタデータ", "xpack.fleet.agentDetails.platformLabel": "プラットフォーム", "xpack.fleet.agentDetails.policyLabel": "ポリシー", "xpack.fleet.agentDetails.releaseLabel": "エージェントリリース", "xpack.fleet.agentDetails.statusLabel": "ステータス", - "xpack.fleet.agentDetails.subTabs.activityLogTab": "アクティビティログ", "xpack.fleet.agentDetails.subTabs.detailsTab": "エージェントの詳細", "xpack.fleet.agentDetails.unexceptedErrorTitle": "エージェントの読み込み中にエラーが発生しました", "xpack.fleet.agentDetails.upgradeAvailableTooltip": "アップグレードが利用可能です", - "xpack.fleet.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ", "xpack.fleet.agentDetails.versionLabel": "エージェントバージョン", "xpack.fleet.agentDetails.viewAgentListTitle": "すべてのエージェントを表示", "xpack.fleet.agentEnrollment.agentDescription": "Elasticエージェントをホストに追加し、データを収集して、Elastic Stackに送信します。", @@ -7214,32 +7204,6 @@ "xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle": "Elasticエージェントを登録して実行", "xpack.fleet.agentEnrollment.stepRunAgentDescription": "エージェントのディレクトリから、このコマンドを実行し、Elasticエージェントを、インストール、登録、起動します。このコマンドを再利用すると、複数のホストでエージェントを設定できます。管理者権限が必要です。", "xpack.fleet.agentEnrollment.stepRunAgentTitle": "エージェントの起動", - "xpack.fleet.agentEventsList.collapseDetailsAriaLabel": "詳細を非表示", - "xpack.fleet.agentEventsList.expandDetailsAriaLabel": "詳細を表示", - "xpack.fleet.agentEventsList.messageColumnTitle": "メッセージ", - "xpack.fleet.agentEventsList.messageDetailsTitle": "メッセージ", - "xpack.fleet.agentEventsList.payloadDetailsTitle": "ペイロード", - "xpack.fleet.agentEventsList.refreshButton": "更新", - "xpack.fleet.agentEventsList.searchPlaceholderText": "アクティビティログを検索", - "xpack.fleet.agentEventsList.subtypeColumnTitle": "サブタイプ", - "xpack.fleet.agentEventsList.timestampColumnTitle": "タイムスタンプ", - "xpack.fleet.agentEventsList.typeColumnTitle": "タイプ", - "xpack.fleet.agentEventSubtype.acknowledgedLabel": "認識", - "xpack.fleet.agentEventSubtype.dataDumpLabel": "データダンプ", - "xpack.fleet.agentEventSubtype.degradedLabel": "劣化", - "xpack.fleet.agentEventSubtype.failedLabel": "失敗", - "xpack.fleet.agentEventSubtype.inProgressLabel": "進行中", - "xpack.fleet.agentEventSubtype.policyLabel": "ポリシー", - "xpack.fleet.agentEventSubtype.runningLabel": "実行中", - "xpack.fleet.agentEventSubtype.startingLabel": "開始中", - "xpack.fleet.agentEventSubtype.stoppedLabel": "停止", - "xpack.fleet.agentEventSubtype.stoppingLabel": "停止中", - "xpack.fleet.agentEventSubtype.unknownLabel": "不明", - "xpack.fleet.agentEventSubtype.updatingLabel": "更新中", - "xpack.fleet.agentEventType.actionLabel": "アクション", - "xpack.fleet.agentEventType.actionResultLabel": "アクション結果", - "xpack.fleet.agentEventType.errorLabel": "エラー", - "xpack.fleet.agentEventType.stateLabel": "ステータス", "xpack.fleet.agentHealth.checkInTooltipText": "前回のチェックイン {lastCheckIn}", "xpack.fleet.agentHealth.degradedStatusText": "劣化", "xpack.fleet.agentHealth.enrollingStatusText": "登録中", @@ -7585,10 +7549,6 @@ "xpack.fleet.invalidLicenseTitle": "ライセンスの期限切れ", "xpack.fleet.listTabs.agentTitle": "エージェント", "xpack.fleet.listTabs.enrollmentTokensTitle": "登録トークン", - "xpack.fleet.metadataForm.addButton": "+ メタデータを追加", - "xpack.fleet.metadataForm.keyLabel": "キー", - "xpack.fleet.metadataForm.submitButtonText": "追加", - "xpack.fleet.metadataForm.valueLabel": "値", "xpack.fleet.namespaceValidation.invalidCharactersErrorMessage": "名前空間に無効な文字が含まれています", "xpack.fleet.namespaceValidation.lowercaseErrorMessage": "名前空間は小文字で指定する必要があります", "xpack.fleet.namespaceValidation.requiredErrorMessage": "名前空間は必須です", @@ -19494,307 +19454,85 @@ "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。", "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "アラートの事前構成タイトル。", "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "しきい値を超えた値。", - "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "アラート{name}グループ{group}値{value}が{date}に{window}にわたってしきい値{function}を超えました", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート{name}グループ{group}がしきい値を超えました", "xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値", + "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました: {comparator}", + "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です", + "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "境界名を選択", + "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", + "xpack.stackAlerts.geoThreshold.delayOffset": "遅延評価オフセット", + "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", + "xpack.stackAlerts.geoThreshold.entityByLabel": "グループ基準", + "xpack.stackAlerts.geoThreshold.entityIndexLabel": "インデックス", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", + "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "エンティティは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", + "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", + "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空間フィールド", + "xpack.stackAlerts.geoThreshold.indexLabel": "インデックス", + "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "インデックスパターン", + "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", + "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "追跡しきい値", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", + "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", + "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "境界を選択:", + "xpack.stackAlerts.geoThreshold.selectEntity": "エンティティを選択", + "xpack.stackAlerts.geoThreshold.selectGeoLabel": "ジオフィールドを選択", + "xpack.stackAlerts.geoThreshold.selectIndex": "条件を定義してください", + "xpack.stackAlerts.geoThreshold.selectLabel": "ジオフィールドを選択", + "xpack.stackAlerts.geoThreshold.selectOffset": "オフセットを選択(任意)", + "xpack.stackAlerts.geoThreshold.selectTimeLabel": "時刻フィールドを選択", + "xpack.stackAlerts.geoThreshold.timeFieldLabel": "時間フィールド", + "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", + "xpack.stackAlerts.geoThreshold.whenEntityLabel": "エンティティ", + "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "集約フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "インデックスが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "用語サイズが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "しきい値 0 が必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "しきい値 1 が必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "時間フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "用語フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.conditionPrompt": "条件を定義してください", + "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", + "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "閉じる", + "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", + "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", + "xpack.stackAlerts.threshold.ui.selectIndex": "インデックスを選択してください", + "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "閉じる", + "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", + "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "インデックス", + "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "インデックス", + "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "クエリを実行するインデックス", + "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "時間フィールド", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", + "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", + "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", "xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です", "xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」", "xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります", "xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", - "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました: {comparator}", "xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "無効な日付{date}", "xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "無効な期間:「{duration}」", "xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」", "xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。", - "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です", "xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」", "xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です", "xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です", "xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です", - "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", - "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", - "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", - "xpack.transform.actionDeleteTransform.deleteDestIndexPatternTitle": "インデックスパターン{destinationIndex}の削除", - "xpack.transform.agg.popoverForm.aggLabel": "集約", - "xpack.transform.agg.popoverForm.aggNameAlreadyUsedError": "別の集約で既に同じ名前が使用されています。", - "xpack.transform.agg.popoverForm.aggNameInvalidCharError": "無効な名前です。「[」、「]」「>」は使用できず、名前の始めと終わりにはスペースを使用できません。", - "xpack.transform.agg.popoverForm.fieldLabel": "フィールド", - "xpack.transform.agg.popoverForm.filerAgg.range.greaterThanLabel": "より大きい", - "xpack.transform.agg.popoverForm.filerAgg.range.lessThanLabel": "より小さい", - "xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions": "候補を取得できません", - "xpack.transform.agg.popoverForm.filerAgg.term.valueLabel": "値", - "xpack.transform.agg.popoverForm.filerAggLabel": "フィルタークエリ", - "xpack.transform.agg.popoverForm.nameLabel": "集約名", - "xpack.transform.agg.popoverForm.percentsLabel": "パーセント", - "xpack.transform.agg.popoverForm.submitButtonLabel": "適用", - "xpack.transform.agg.popoverForm.unsupportedAggregationHelpText": "このフォームでは集約名のみを編集できます。詳細エディターを使用して、集約の他の部分を編集してください。", - "xpack.transform.aggLabelForm.deleteItemAriaLabel": "アイテムを削除", - "xpack.transform.aggLabelForm.editAggAriaLabel": "集約を編集", - "xpack.transform.app.checkingPrivilegesDescription": "権限を確認中…", - "xpack.transform.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得中にエラーが発生。", - "xpack.transform.app.deniedPrivilegeDescription": "Transforms のこのセクションを使用するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {missingPrivileges}。", - "xpack.transform.app.deniedPrivilegeTitle": "クラスター特権が足りません", - "xpack.transform.appName": "データフレームジョブ", - "xpack.transform.appTitle": "変換", - "xpack.transform.capability.noPermission.createTransformTooltip": "データフレーム変換を作成するパーミッションがありません。", - "xpack.transform.capability.noPermission.deleteTransformTooltip": "データフレーム変換を削除するパーミッションがありません。", - "xpack.transform.capability.noPermission.startOrStopTransformTooltip": "データフレーム変換を開始・停止するパーミッションがありません。", - "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message} 管理者にお問い合わせください。", - "xpack.transform.clone.errorPromptText": "ソースインデックスパターンが存在するかどうかを確認するときにエラーが発生しました", - "xpack.transform.clone.errorPromptTitle": "変換構成の取得中にエラーが発生しました。", - "xpack.transform.clone.fetchErrorPromptText": "KibanaインデックスパターンIDを取得できませんでした。", - "xpack.transform.clone.noIndexPatternErrorPromptText": "変換を複製できません{indexPattern}のインデックスパターンが存在しません。", - "xpack.transform.cloneTransform.breadcrumbTitle": "クローン変換", - "xpack.transform.createTransform.breadcrumbTitle": "変換の作成", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage": "インデックスパターン{destinationIndex}の削除中にエラーが発生しました", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage": "インデックスパターン{destinationIndex}を削除する要求が確認されました。", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", - "xpack.transform.deleteTransform.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}", - "xpack.transform.description": "説明", - "xpack.transform.groupby.popoverForm.aggLabel": "集約", - "xpack.transform.groupBy.popoverForm.aggNameAlreadyUsedError": "別のグループ分けの構成が既にこの名前を使用しています。", - "xpack.transform.groupBy.popoverForm.aggNameInvalidCharError": "無効な名前です。「[」、「]」「>」は使用できず、名前の始めと終わりにはスペースを使用できません。", - "xpack.transform.groupBy.popoverForm.fieldLabel": "フィールド", - "xpack.transform.groupBy.popoverForm.intervalError": "無効な間隔。", - "xpack.transform.groupBy.popoverForm.intervalLabel": "間隔", - "xpack.transform.groupBy.popoverForm.intervalPercents": "パーセンタイルをコンマで区切って列記します。", - "xpack.transform.groupBy.popoverForm.nameLabel": "グループ分け名", - "xpack.transform.groupBy.popoverForm.submitButtonLabel": "適用", - "xpack.transform.groupBy.popoverForm.unsupportedGroupByHelpText": "このフォームでは group_by 名のみを編集できます。詳細エディターを使用して、group_by 構成の他の部分を編集してください。", - "xpack.transform.groupByLabelForm.deleteItemAriaLabel": "アイテムを削除", - "xpack.transform.groupByLabelForm.editIntervalAriaLabel": "間隔を編集", - "xpack.transform.home.breadcrumbTitle": "データフレームジョブ", - "xpack.transform.indexPreview.copyClipboardTooltip": "インデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.licenseCheckErrorMessage": "ライセンス確認失敗", - "xpack.transform.list.emptyPromptButtonText": "初めての変換を作成してみましょう。", - "xpack.transform.list.emptyPromptTitle": "変換が見つかりません", - "xpack.transform.list.errorPromptTitle": "変換リストの取得中にエラーが発生しました。", - "xpack.transform.mode": "モード", - "xpack.transform.modeFilter": "モード", - "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "他のすべてのリクエストはキャンセルされました。", - "xpack.transform.models.transformService.requestToActionTimedOutErrorMessage": "「{id}」を{action}するリクエストがタイムアウトしました。{extra}", - "xpack.transform.multiTransformActionsMenu.managementActionsAriaLabel": "管理アクション", - "xpack.transform.multiTransformActionsMenu.transformsCount": "{count} 件の{count, plural, one {変換} other {変換}}を選択済み", - "xpack.transform.newTransform.chooseSourceTitle": "ソースの選択", - "xpack.transform.newTransform.newTransformTitle": "新規変換", - "xpack.transform.newTransform.searchSelection.notFoundLabel": "一致するインデックスまたは保存検索が見つかりませんでした。", - "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", - "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", - "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", - "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", - "xpack.transform.progress": "進捗", - "xpack.transform.statsBar.batchTransformsLabel": "一斉", - "xpack.transform.statsBar.continuousTransformsLabel": "連続", - "xpack.transform.statsBar.failedTransformsLabel": "失敗", - "xpack.transform.statsBar.startedTransformsLabel": "開始済み", - "xpack.transform.statsBar.totalTransformsLabel": "変換合計", - "xpack.transform.status": "ステータス", - "xpack.transform.statusFilter": "ステータス", - "xpack.transform.stepCreateForm.continuousModeLabel": "連続モード", - "xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton": "クリップボードにコピー", - "xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription": "ジョブを作成する Kibana 開発コンソールのコマンドをクリップボードにコピーします。", - "xpack.transform.stepCreateForm.createAndStartTransformButton": "作成して開始", - "xpack.transform.stepCreateForm.createAndStartTransformDescription": "変換を作成して開始します。変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。変換の開始後、変換の閲覧を続けるオプションが提供されます。", - "xpack.transform.stepCreateForm.createIndexPatternErrorMessage": "Kibana インデックスパターン {indexPatternName} の作成中にエラーが発生しました:", - "xpack.transform.stepCreateForm.createIndexPatternLabel": "インデックスパターンを作成", - "xpack.transform.stepCreateForm.createIndexPatternSuccessMessage": "Kibana インデックスパターン {indexPatternName} が作成されました", - "xpack.transform.stepCreateForm.createTransformButton": "作成", - "xpack.transform.stepCreateForm.createTransformDescription": "変換を開始せずに作成します。変換は後程変換リストに戻って開始できます。", - "xpack.transform.stepCreateForm.createTransformErrorMessage": "変換 {transformId} の取得中にエラーが発生しました。", - "xpack.transform.stepCreateForm.createTransformSuccessMessage": "変換 {transformId} の作成リクエストが受け付けられました。", - "xpack.transform.stepCreateForm.creatingIndexPatternMessage": "Kibana インデックスパターンを作成中…", - "xpack.transform.stepCreateForm.discoverCardDescription": "ディスカバリでデータフレームピボットを閲覧します。", - "xpack.transform.stepCreateForm.discoverCardTitle": "ディスカバー", - "xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage": "Kibana インデックスパターン {indexPatternName} の作成中にエラーが発生しました:インデックスパターンが既に存在します。", - "xpack.transform.stepCreateForm.progressErrorMessage": "進捗パーセンテージの取得中にエラーが発生しました:", - "xpack.transform.stepCreateForm.progressTitle": "進捗", - "xpack.transform.stepCreateForm.startTransformButton": "開始", - "xpack.transform.stepCreateForm.startTransformDescription": "変換を開始します。変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。変換の開始後、変換の閲覧を続けるオプションが提供されます。", - "xpack.transform.stepCreateForm.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました。", - "xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage": "変換開始要求の呼び出し中にエラーが発生しました。", - "xpack.transform.stepCreateForm.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", - "xpack.transform.stepCreateForm.transformListCardDescription": "データフレームジョブの管理ページに戻ります。", - "xpack.transform.stepCreateForm.transformListCardTitle": "データフレームジョブ", - "xpack.transform.stepDefineForm.addSubAggregationPlaceholder": "下位集約を追加...", - "xpack.transform.stepDefineForm.advancedEditorApplyButtonText": "変更を適用", - "xpack.transform.stepDefineForm.advancedEditorAriaLabel": "高度なピボットエディター", - "xpack.transform.stepDefineForm.advancedEditorHelpText": "詳細エディターでは、変換のピボット構成を編集できます。", - "xpack.transform.stepDefineForm.advancedEditorHelpTextLink": "使用可能なオプションの詳細を確認してください。", - "xpack.transform.stepDefineForm.advancedEditorLabel": "ピボット構成オブジェクト", - "xpack.transform.stepDefineForm.advancedEditorSourceConfigSwitchLabel": "JSONクエリを編集", - "xpack.transform.stepDefineForm.advancedEditorSwitchLabel": "JSON構成を編集", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalBodyText": "詳細エディターの変更は適用されませんでした。詳細エディターを無効にすると、編集内容が失われます。", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalCancelButtonText": "キャンセル", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalConfirmButtonText": "詳細エディターを無効にする", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalTitle": "適用されていない変更", - "xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText": "変更を適用", - "xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel": "クエリの詳細エディター", - "xpack.transform.stepDefineForm.advancedSourceEditorHelpText": "高度なエディターでは、変換構成のソースクエリ句を編集できます。", - "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalBodyText": "クエリバーに戻すと、編集内容が失われます。", - "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalConfirmButtonText": "クエリバーに切り替え", - "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalTitle": "編集内容は失われます", - "xpack.transform.stepDefineForm.aggExistsErrorMessage": "「{aggName}」という名前の集約構成は既に存在します。", - "xpack.transform.stepDefineForm.aggregationsLabel": "アグリゲーション(集計)", - "xpack.transform.stepDefineForm.aggregationsPlaceholder": "集約を追加…", - "xpack.transform.stepDefineForm.groupByExistsErrorMessage": "「{aggName}」という名前のグループ分け構成は既に存在します。", - "xpack.transform.stepDefineForm.groupByLabel": "グループ分けの条件", - "xpack.transform.stepDefineForm.groupByPlaceholder": "グループ分けの条件フィールドを追加…", - "xpack.transform.stepDefineForm.indexPatternLabel": "インデックスパターン", - "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ:{errorMessage}", - "xpack.transform.stepDefineForm.maxSubAggsLevelsLimitMessage": "フォームで追加できる下位集約の最大レベル数に達しました。別のレベルを追加する場合は、JSON構成を編集してください。", - "xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage": "「{aggListName}」とネスティングの矛盾があるため、構成「{aggName}」を追加できませんでした。", - "xpack.transform.stepDefineForm.nestedConflictErrorMessage": "「{aggNameCheck}」とネスティングの矛盾があるため、構成「{aggName}」を追加できませんでした。", - "xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage": "「{groupByListName}」とネスティングの矛盾があるため、構成「{aggName}」を追加できませんでした。", - "xpack.transform.stepDefineForm.queryPlaceholderKql": "例: {example}", - "xpack.transform.stepDefineForm.queryPlaceholderLucene": "例: {example}", - "xpack.transform.stepDefineForm.savedSearchLabel": "保存検索", - "xpack.transform.stepDefineSummary.aggregationsLabel": "アグリゲーション(集計)", - "xpack.transform.stepDefineSummary.groupByLabel": "グループ分けの条件", - "xpack.transform.stepDefineSummary.indexPatternLabel": "インデックスパターン", - "xpack.transform.stepDefineSummary.queryCodeBlockLabel": "クエリ", - "xpack.transform.stepDefineSummary.queryLabel": "クエリ", - "xpack.transform.stepDefineSummary.savedSearchLabel": "保存検索", - "xpack.transform.stepDetailsForm.advancedSettingsAccordionButtonContent": "高度な設定", - "xpack.transform.stepDetailsForm.continuousModeAriaLabel": "遅延を選択してください。", - "xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText": "新しいドキュメントを特定するために使用できる日付フィールドを選択してください。", - "xpack.transform.stepDetailsForm.continuousModeDateFieldLabel": "日付フィールド", - "xpack.transform.stepDetailsForm.continuousModeDelayError": "無効な遅延フォーマット", - "xpack.transform.stepDetailsForm.continuousModeDelayHelpText": "現在の時刻と最新のインプットデータ時刻の間の遅延です。", - "xpack.transform.stepDetailsForm.continuousModeDelayLabel": "遅延", - "xpack.transform.stepDetailsForm.continuousModeError": "日付フィールドがないインデックスでは、連続モードを使用できません。", - "xpack.transform.stepDetailsForm.destinationIndexHelpText": "この名前のインデックスが既に存在します。この変換を実行すると、デスティネーションインデックスが変更されます。", - "xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel": "固有の宛先インデックス名を選択してください。", - "xpack.transform.stepDetailsForm.destinationIndexInvalidError": "無効なデスティネーションインデックス名。", - "xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink": "インデックス名の制限に関する詳細。", - "xpack.transform.stepDetailsForm.destinationIndexLabel": "デスティネーションインデックス", - "xpack.transform.stepDetailsForm.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}", - "xpack.transform.stepDetailsForm.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}", - "xpack.transform.stepDetailsForm.errorGettingIndexNames": "既存のインデックス名の取得中にエラーが発生しました:", - "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", - "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", - "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "変換プレビューの取得中にエラーが発生しました。", - "xpack.transform.stepDetailsForm.frequencyAriaLabel": "頻度を選択してください。", - "xpack.transform.stepDetailsForm.frequencyError": "無効な頻度形式", - "xpack.transform.stepDetailsForm.frequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", - "xpack.transform.stepDetailsForm.frequencyLabel": "頻度", - "xpack.transform.stepDetailsForm.indexPatternTimeFieldHelpText": "グローバル時間フィルターで使用するためのプライマリ時間フィールドを選択してください。", - "xpack.transform.stepDetailsForm.indexPatternTimeFieldLabel": "時間フィールド", - "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel": "最大ページ検索サイズを選択してください。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeError": "max_page_search_sizeは10~10000の範囲の数値でなければなりません。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeHelpText": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeLabel": "最大ページ検索サイズ", - "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "時間フィルターを使用しない", - "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", - "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", - "xpack.transform.stepDetailsForm.transformDescriptionPlaceholderText": "説明(オプション)", - "xpack.transform.stepDetailsForm.transformIdExistsError": "この ID の変換が既に存在します。", - "xpack.transform.stepDetailsForm.transformIdInputAriaLabel": "固有のジョブ ID を選択してください。", - "xpack.transform.stepDetailsForm.transformIdInvalidError": "小文字のアルファベットと数字 (a-z と 0-9)、ハイフンまたはアンダーラインのみ使用でき、最初と最後を英数字にする必要があります。", - "xpack.transform.stepDetailsForm.transformIdLabel": "ジョブ ID", - "xpack.transform.stepDetailsSummary.advancedSettingsAccordionButtonContent": "高度な設定", - "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "連続モード日付フィールド", - "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "このジョブの Kibana インデックスパターンが作成されます。", - "xpack.transform.stepDetailsSummary.destinationIndexLabel": "デスティネーションインデックス", - "xpack.transform.stepDetailsSummary.frequencyLabel": "頻度", - "xpack.transform.stepDetailsSummary.indexPatternTimeFieldLabel": "Kibanaインデックスパターン時間フィールド", - "xpack.transform.stepDetailsSummary.maxPageSearchSizeLabel": "最大ページ検索サイズ", - "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "変換の説明", - "xpack.transform.stepDetailsSummary.transformIdLabel": "ジョブ ID", - "xpack.transform.tableActionLabel": "アクション", - "xpack.transform.toastText.closeModalButtonText": "閉じる", - "xpack.transform.toastText.modalTitle": "詳細を入力", - "xpack.transform.toastText.openModalButtonText": "詳細を表示", - "xpack.transform.transformForm.sizeNotationPlaceholder": "例: {example1}、{example2}、{example3}、{example4}", - "xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage": "{count}個のディスティネーションインデックス{count, plural, one {パターン} other {パターン}}を正常に削除しました。", - "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "{count}個のディスティネーション{count, plural, one {インデックス} other {インデックス}}を正常に削除しました。", - "xpack.transform.transformList.bulkDeleteModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を削除", - "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "{count} {count, plural, one {個の変換} other {個の変換}}を正常に削除しました。", - "xpack.transform.transformList.bulkStartModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を開始", - "xpack.transform.transformList.cloneActionNameText": "クローンを作成", - "xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "1 つまたは複数の変換が完了済みの一斉変換で、再度開始できません。", - "xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} は完了済みの一斉変換で、再度開始できません。", - "xpack.transform.transformList.createTransformButton": "変換の作成", - "xpack.transform.transformList.deleteActionDisabledToolTipContent": "削除するにはデータフレームジョブを停止してください。", - "xpack.transform.transformList.deleteActionNameText": "削除", - "xpack.transform.transformList.deleteBulkActionDisabledToolTipContent": "削除するには、選択された変換のうちの 1 つまたは複数を停止する必要があります。", - "xpack.transform.transformList.deleteModalCancelButton": "キャンセル", - "xpack.transform.transformList.deleteModalDeleteButton": "削除", - "xpack.transform.transformList.deleteModalTitle": "{transformId}を削除しますか?", - "xpack.transform.transformList.deleteTransformErrorMessage": "変換 {transformId} の削除中にエラーが発生しました", - "xpack.transform.transformList.deleteTransformGenericErrorMessage": "変換を削除するための API エンドポイントの呼び出し中にエラーが発生しました。", - "xpack.transform.transformList.deleteTransformSuccessMessage": "変換 {transformId} の削除リクエストが受け付けられました。", - "xpack.transform.transformList.editActionNameText": "編集", - "xpack.transform.transformList.editFlyoutCalloutDocs": "ドキュメントを表示", - "xpack.transform.transformList.editFlyoutCalloutText": "このフォームでは、変換を更新できます。更新できるプロパティのリストは、変換を作成するときに定義できるリストのサブセットです。", - "xpack.transform.transformList.editFlyoutCancelButtonText": "キャンセル", - "xpack.transform.transformList.editFlyoutFormAdvancedSettingsButtonContent": "高度な設定", - "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "説明", - "xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "ディスティネーション構成", - "xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "デスティネーションインデックス", - "xpack.transform.transformList.editFlyoutFormDestinationPipelineLabel": "パイプライン", - "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "スロットリングを有効にするには、毎秒入力するドキュメントの上限を設定します。", - "xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "毎秒あたりのドキュメント", - "xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", - "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "頻度", - "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "頻度値が無効です。", - "xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}", - "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。", - "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大ページ検索サイズ", - "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}", - "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "値は1以上の整数でなければなりません。", - "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "値は10~10000の範囲の整数でなければなりません。", - "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必須フィールド。", - "xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage": "値は文字列型でなければなりません。", - "xpack.transform.transformList.editFlyoutTitle": "{transformId}を編集", - "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", - "xpack.transform.transformList.editTransformGenericErrorMessage": "変換を削除するためのAPIエンドポイントの呼び出し中にエラーが発生しました。", - "xpack.transform.transformList.editTransformSuccessMessage": "変換{transformId}が更新されました。", - "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "ユーザーがディスティネーションインデックスを削除できるかどうかを確認するときにエラーが発生しました。", - "xpack.transform.transformList.refreshButtonLabel": "更新", - "xpack.transform.transformList.rowCollapse": "{transformId} の詳細を非表示", - "xpack.transform.transformList.rowExpand": "{transformId} の詳細を表示", - "xpack.transform.transformList.showDetailsColumn.screenReaderDescription": "このカラムには変換ごとの詳細を示すクリック可能なコントロールが含まれます", - "xpack.transform.transformList.startActionNameText": "開始", - "xpack.transform.transformList.startedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", - "xpack.transform.transformList.startedTransformToolTip": "{transformId} は既に開始済みです。", - "xpack.transform.transformList.startModalBody": "変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。", - "xpack.transform.transformList.startModalCancelButton": "キャンセル", - "xpack.transform.transformList.startModalStartButton": "開始", - "xpack.transform.transformList.startModalTitle": "{transformId}を開始しますか?", - "xpack.transform.transformList.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました", - "xpack.transform.transformList.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", - "xpack.transform.transformList.stopActionNameText": "終了", - "xpack.transform.transformList.stoppedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", - "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} は既に停止済みです。", - "xpack.transform.transformList.stopTransformErrorMessage": "データフレーム変換 {transformId} の停止中にエラーが発生しました", - "xpack.transform.transformList.stopTransformResponseSchemaErrorMessage": "変換停止要求の呼び出し中にエラーが発生しました。", - "xpack.transform.transformList.stopTransformSuccessMessage": "データフレーム変換 {transformId} の停止リクエストが受け付けられました。", - "xpack.transform.transformList.transformDescription": "変換を使用して、集約されたインデックスまたはエンティティ中心のインデックスに、既存のElasticsearchインデックスをインデックスします。", - "xpack.transform.transformList.transformDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", - "xpack.transform.transformList.transformDetails.messagesPane.messageLabel": "メッセージ", - "xpack.transform.transformList.transformDetails.messagesPane.nodeLabel": "ノード", - "xpack.transform.transformList.transformDetails.messagesPane.timeLabel": "時間", - "xpack.transform.transformList.transformDetails.tabs.transformDetailsLabel": "詳細", - "xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel": "メッセージ", - "xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel": "プレビュー", - "xpack.transform.transformList.transformDetails.tabs.transformStatsLabel": "統計", - "xpack.transform.transformList.transformDocsLinkText": "変換ドキュメント", - "xpack.transform.transformList.transformTitle": "データフレームジョブ", - "xpack.transform.transformsDescription": "変換を使用して、集約されたインデックスまたはエンティティ中心のインデックスに、既存のElasticsearchインデックスをインデックスします。", - "xpack.transform.transformsTitle": "変換", - "xpack.transform.transformsWizard.cloneTransformTitle": "クローン変換", - "xpack.transform.transformsWizard.createTransformTitle": "変換の作成", - "xpack.transform.transformsWizard.stepConfigurationTitle": "構成", - "xpack.transform.transformsWizard.stepCreateTitle": "作成", - "xpack.transform.transformsWizard.stepDetailsTitle": "ジョブの詳細", - "xpack.transform.transformsWizard.transformDocsLinkText": "変換ドキュメント", - "xpack.transform.wizard.nextStepButton": "次へ", - "xpack.transform.wizard.previousStepButton": "前へ", "xpack.triggersActionsUI.actionVariables.alertIdLabel": "アラートの ID。", "xpack.triggersActionsUI.actionVariables.alertInstanceIdLabel": "アラートのアクションを予定したアラートインスタンス ID。", "xpack.triggersActionsUI.actionVariables.alertNameLabel": "アラートの名前。", @@ -20047,42 +19785,6 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "{numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}を回復できません。", - "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "境界名を選択", - "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", - "xpack.stackAlerts.geoThreshold.delayOffset": "遅延評価オフセット", - "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", - "xpack.stackAlerts.geoThreshold.entityByLabel": "グループ基準", - "xpack.stackAlerts.geoThreshold.entityIndexLabel": "インデックス", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", - "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "エンティティは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", - "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", - "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空間フィールド", - "xpack.stackAlerts.geoThreshold.indexLabel": "インデックス", - "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "インデックスパターン", - "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", - "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "追跡しきい値", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", - "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", - "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", - "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", - "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "境界を選択:", - "xpack.stackAlerts.geoThreshold.selectEntity": "エンティティを選択", - "xpack.stackAlerts.geoThreshold.selectGeoLabel": "ジオフィールドを選択", - "xpack.stackAlerts.geoThreshold.selectIndex": "条件を定義してください", - "xpack.stackAlerts.geoThreshold.selectLabel": "ジオフィールドを選択", - "xpack.stackAlerts.geoThreshold.selectOffset": "オフセットを選択(任意)", - "xpack.stackAlerts.geoThreshold.selectTimeLabel": "時刻フィールドを選択", - "xpack.stackAlerts.geoThreshold.timeFieldLabel": "時間フィールド", - "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", - "xpack.stackAlerts.geoThreshold.whenEntityLabel": "エンティティ", "xpack.triggersActionsUI.home.alertsTabTitle": "アラート", "xpack.triggersActionsUI.home.appTitle": "アラートとアクション", "xpack.triggersActionsUI.home.breadcrumbTitle": "アラートとアクション", @@ -20125,15 +19827,6 @@ "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "値が必要です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "パスワードが必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "集約フィールドが必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "インデックスが必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "用語サイズが必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "しきい値 0 が必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "しきい値 1 が必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "時間フィールドが必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "用語フィールドが必要です。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} コネクタ", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。", @@ -20142,27 +19835,11 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} コネクター", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", - "xpack.stackAlerts.threshold.ui.conditionPrompt": "条件を定義してください", - "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成", - "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "閉じる", - "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", "xpack.triggersActionsUI.sections.alertAdd.operationName": "作成", - "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", - "xpack.stackAlerts.threshold.ui.selectIndex": "インデックスを選択してください", - "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "閉じる", - "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", - "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "インデックス", - "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "インデックス", - "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "クエリを実行するインデックス", - "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "時間フィールド", "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "フィールドを選択", - "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", - "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", - "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "このアラートは無効になっていて再表示できません。[↑ を無効にする]を切り替えてアクティブにします。", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "期間", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "インスタンス", @@ -20315,6 +19992,289 @@ "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, one {秒} other {秒}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", + "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", + "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", + "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", + "xpack.transform.actionDeleteTransform.deleteDestIndexPatternTitle": "インデックスパターン{destinationIndex}の削除", + "xpack.transform.agg.popoverForm.aggLabel": "集約", + "xpack.transform.agg.popoverForm.aggNameAlreadyUsedError": "別の集約で既に同じ名前が使用されています。", + "xpack.transform.agg.popoverForm.aggNameInvalidCharError": "無効な名前です。「[」、「]」「>」は使用できず、名前の始めと終わりにはスペースを使用できません。", + "xpack.transform.agg.popoverForm.fieldLabel": "フィールド", + "xpack.transform.agg.popoverForm.filerAgg.range.greaterThanLabel": "より大きい", + "xpack.transform.agg.popoverForm.filerAgg.range.lessThanLabel": "より小さい", + "xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions": "候補を取得できません", + "xpack.transform.agg.popoverForm.filerAgg.term.valueLabel": "値", + "xpack.transform.agg.popoverForm.filerAggLabel": "フィルタークエリ", + "xpack.transform.agg.popoverForm.nameLabel": "集約名", + "xpack.transform.agg.popoverForm.percentsLabel": "パーセント", + "xpack.transform.agg.popoverForm.submitButtonLabel": "適用", + "xpack.transform.agg.popoverForm.unsupportedAggregationHelpText": "このフォームでは集約名のみを編集できます。詳細エディターを使用して、集約の他の部分を編集してください。", + "xpack.transform.aggLabelForm.deleteItemAriaLabel": "アイテムを削除", + "xpack.transform.aggLabelForm.editAggAriaLabel": "集約を編集", + "xpack.transform.app.checkingPrivilegesDescription": "権限を確認中…", + "xpack.transform.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得中にエラーが発生。", + "xpack.transform.app.deniedPrivilegeDescription": "Transforms のこのセクションを使用するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {missingPrivileges}。", + "xpack.transform.app.deniedPrivilegeTitle": "クラスター特権が足りません", + "xpack.transform.appName": "データフレームジョブ", + "xpack.transform.appTitle": "変換", + "xpack.transform.capability.noPermission.createTransformTooltip": "データフレーム変換を作成するパーミッションがありません。", + "xpack.transform.capability.noPermission.deleteTransformTooltip": "データフレーム変換を削除するパーミッションがありません。", + "xpack.transform.capability.noPermission.startOrStopTransformTooltip": "データフレーム変換を開始・停止するパーミッションがありません。", + "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message} 管理者にお問い合わせください。", + "xpack.transform.clone.errorPromptText": "ソースインデックスパターンが存在するかどうかを確認するときにエラーが発生しました", + "xpack.transform.clone.errorPromptTitle": "変換構成の取得中にエラーが発生しました。", + "xpack.transform.clone.fetchErrorPromptText": "KibanaインデックスパターンIDを取得できませんでした。", + "xpack.transform.clone.noIndexPatternErrorPromptText": "変換を複製できません{indexPattern}のインデックスパターンが存在しません。", + "xpack.transform.cloneTransform.breadcrumbTitle": "クローン変換", + "xpack.transform.createTransform.breadcrumbTitle": "変換の作成", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage": "インデックスパターン{destinationIndex}の削除中にエラーが発生しました", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage": "インデックスパターン{destinationIndex}を削除する要求が確認されました。", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", + "xpack.transform.deleteTransform.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}", + "xpack.transform.description": "説明", + "xpack.transform.groupby.popoverForm.aggLabel": "集約", + "xpack.transform.groupBy.popoverForm.aggNameAlreadyUsedError": "別のグループ分けの構成が既にこの名前を使用しています。", + "xpack.transform.groupBy.popoverForm.aggNameInvalidCharError": "無効な名前です。「[」、「]」「>」は使用できず、名前の始めと終わりにはスペースを使用できません。", + "xpack.transform.groupBy.popoverForm.fieldLabel": "フィールド", + "xpack.transform.groupBy.popoverForm.intervalError": "無効な間隔。", + "xpack.transform.groupBy.popoverForm.intervalLabel": "間隔", + "xpack.transform.groupBy.popoverForm.intervalPercents": "パーセンタイルをコンマで区切って列記します。", + "xpack.transform.groupBy.popoverForm.nameLabel": "グループ分け名", + "xpack.transform.groupBy.popoverForm.submitButtonLabel": "適用", + "xpack.transform.groupBy.popoverForm.unsupportedGroupByHelpText": "このフォームでは group_by 名のみを編集できます。詳細エディターを使用して、group_by 構成の他の部分を編集してください。", + "xpack.transform.groupByLabelForm.deleteItemAriaLabel": "アイテムを削除", + "xpack.transform.groupByLabelForm.editIntervalAriaLabel": "間隔を編集", + "xpack.transform.home.breadcrumbTitle": "データフレームジョブ", + "xpack.transform.indexPreview.copyClipboardTooltip": "インデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", + "xpack.transform.licenseCheckErrorMessage": "ライセンス確認失敗", + "xpack.transform.list.emptyPromptButtonText": "初めての変換を作成してみましょう。", + "xpack.transform.list.emptyPromptTitle": "変換が見つかりません", + "xpack.transform.list.errorPromptTitle": "変換リストの取得中にエラーが発生しました。", + "xpack.transform.mode": "モード", + "xpack.transform.modeFilter": "モード", + "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "他のすべてのリクエストはキャンセルされました。", + "xpack.transform.models.transformService.requestToActionTimedOutErrorMessage": "「{id}」を{action}するリクエストがタイムアウトしました。{extra}", + "xpack.transform.multiTransformActionsMenu.managementActionsAriaLabel": "管理アクション", + "xpack.transform.multiTransformActionsMenu.transformsCount": "{count} 件の{count, plural, one {変換} other {変換}}を選択済み", + "xpack.transform.newTransform.chooseSourceTitle": "ソースの選択", + "xpack.transform.newTransform.newTransformTitle": "新規変換", + "xpack.transform.newTransform.searchSelection.notFoundLabel": "一致するインデックスまたは保存検索が見つかりませんでした。", + "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", + "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", + "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", + "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", + "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", + "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", + "xpack.transform.progress": "進捗", + "xpack.transform.statsBar.batchTransformsLabel": "一斉", + "xpack.transform.statsBar.continuousTransformsLabel": "連続", + "xpack.transform.statsBar.failedTransformsLabel": "失敗", + "xpack.transform.statsBar.startedTransformsLabel": "開始済み", + "xpack.transform.statsBar.totalTransformsLabel": "変換合計", + "xpack.transform.status": "ステータス", + "xpack.transform.statusFilter": "ステータス", + "xpack.transform.stepCreateForm.continuousModeLabel": "連続モード", + "xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton": "クリップボードにコピー", + "xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription": "ジョブを作成する Kibana 開発コンソールのコマンドをクリップボードにコピーします。", + "xpack.transform.stepCreateForm.createAndStartTransformButton": "作成して開始", + "xpack.transform.stepCreateForm.createAndStartTransformDescription": "変換を作成して開始します。変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。変換の開始後、変換の閲覧を続けるオプションが提供されます。", + "xpack.transform.stepCreateForm.createIndexPatternErrorMessage": "Kibana インデックスパターン {indexPatternName} の作成中にエラーが発生しました:", + "xpack.transform.stepCreateForm.createIndexPatternLabel": "インデックスパターンを作成", + "xpack.transform.stepCreateForm.createIndexPatternSuccessMessage": "Kibana インデックスパターン {indexPatternName} が作成されました", + "xpack.transform.stepCreateForm.createTransformButton": "作成", + "xpack.transform.stepCreateForm.createTransformDescription": "変換を開始せずに作成します。変換は後程変換リストに戻って開始できます。", + "xpack.transform.stepCreateForm.createTransformErrorMessage": "変換 {transformId} の取得中にエラーが発生しました。", + "xpack.transform.stepCreateForm.createTransformSuccessMessage": "変換 {transformId} の作成リクエストが受け付けられました。", + "xpack.transform.stepCreateForm.creatingIndexPatternMessage": "Kibana インデックスパターンを作成中…", + "xpack.transform.stepCreateForm.discoverCardDescription": "ディスカバリでデータフレームピボットを閲覧します。", + "xpack.transform.stepCreateForm.discoverCardTitle": "ディスカバー", + "xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage": "Kibana インデックスパターン {indexPatternName} の作成中にエラーが発生しました:インデックスパターンが既に存在します。", + "xpack.transform.stepCreateForm.progressErrorMessage": "進捗パーセンテージの取得中にエラーが発生しました:", + "xpack.transform.stepCreateForm.progressTitle": "進捗", + "xpack.transform.stepCreateForm.startTransformButton": "開始", + "xpack.transform.stepCreateForm.startTransformDescription": "変換を開始します。変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。変換の開始後、変換の閲覧を続けるオプションが提供されます。", + "xpack.transform.stepCreateForm.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました。", + "xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage": "変換開始要求の呼び出し中にエラーが発生しました。", + "xpack.transform.stepCreateForm.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", + "xpack.transform.stepCreateForm.transformListCardDescription": "データフレームジョブの管理ページに戻ります。", + "xpack.transform.stepCreateForm.transformListCardTitle": "データフレームジョブ", + "xpack.transform.stepDefineForm.addSubAggregationPlaceholder": "下位集約を追加...", + "xpack.transform.stepDefineForm.advancedEditorApplyButtonText": "変更を適用", + "xpack.transform.stepDefineForm.advancedEditorAriaLabel": "高度なピボットエディター", + "xpack.transform.stepDefineForm.advancedEditorHelpText": "詳細エディターでは、変換のピボット構成を編集できます。", + "xpack.transform.stepDefineForm.advancedEditorHelpTextLink": "使用可能なオプションの詳細を確認してください。", + "xpack.transform.stepDefineForm.advancedEditorLabel": "ピボット構成オブジェクト", + "xpack.transform.stepDefineForm.advancedEditorSourceConfigSwitchLabel": "JSONクエリを編集", + "xpack.transform.stepDefineForm.advancedEditorSwitchLabel": "JSON構成を編集", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalBodyText": "詳細エディターの変更は適用されませんでした。詳細エディターを無効にすると、編集内容が失われます。", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalCancelButtonText": "キャンセル", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalConfirmButtonText": "詳細エディターを無効にする", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalTitle": "適用されていない変更", + "xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText": "変更を適用", + "xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel": "クエリの詳細エディター", + "xpack.transform.stepDefineForm.advancedSourceEditorHelpText": "高度なエディターでは、変換構成のソースクエリ句を編集できます。", + "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalBodyText": "クエリバーに戻すと、編集内容が失われます。", + "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalConfirmButtonText": "クエリバーに切り替え", + "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalTitle": "編集内容は失われます", + "xpack.transform.stepDefineForm.aggExistsErrorMessage": "「{aggName}」という名前の集約構成は既に存在します。", + "xpack.transform.stepDefineForm.aggregationsLabel": "アグリゲーション(集計)", + "xpack.transform.stepDefineForm.aggregationsPlaceholder": "集約を追加…", + "xpack.transform.stepDefineForm.groupByExistsErrorMessage": "「{aggName}」という名前のグループ分け構成は既に存在します。", + "xpack.transform.stepDefineForm.groupByLabel": "グループ分けの条件", + "xpack.transform.stepDefineForm.groupByPlaceholder": "グループ分けの条件フィールドを追加…", + "xpack.transform.stepDefineForm.indexPatternLabel": "インデックスパターン", + "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ:{errorMessage}", + "xpack.transform.stepDefineForm.maxSubAggsLevelsLimitMessage": "フォームで追加できる下位集約の最大レベル数に達しました。別のレベルを追加する場合は、JSON構成を編集してください。", + "xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage": "「{aggListName}」とネスティングの矛盾があるため、構成「{aggName}」を追加できませんでした。", + "xpack.transform.stepDefineForm.nestedConflictErrorMessage": "「{aggNameCheck}」とネスティングの矛盾があるため、構成「{aggName}」を追加できませんでした。", + "xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage": "「{groupByListName}」とネスティングの矛盾があるため、構成「{aggName}」を追加できませんでした。", + "xpack.transform.stepDefineForm.queryPlaceholderKql": "例: {example}", + "xpack.transform.stepDefineForm.queryPlaceholderLucene": "例: {example}", + "xpack.transform.stepDefineForm.savedSearchLabel": "保存検索", + "xpack.transform.stepDefineSummary.aggregationsLabel": "アグリゲーション(集計)", + "xpack.transform.stepDefineSummary.groupByLabel": "グループ分けの条件", + "xpack.transform.stepDefineSummary.indexPatternLabel": "インデックスパターン", + "xpack.transform.stepDefineSummary.queryCodeBlockLabel": "クエリ", + "xpack.transform.stepDefineSummary.queryLabel": "クエリ", + "xpack.transform.stepDefineSummary.savedSearchLabel": "保存検索", + "xpack.transform.stepDetailsForm.advancedSettingsAccordionButtonContent": "高度な設定", + "xpack.transform.stepDetailsForm.continuousModeAriaLabel": "遅延を選択してください。", + "xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText": "新しいドキュメントを特定するために使用できる日付フィールドを選択してください。", + "xpack.transform.stepDetailsForm.continuousModeDateFieldLabel": "日付フィールド", + "xpack.transform.stepDetailsForm.continuousModeDelayError": "無効な遅延フォーマット", + "xpack.transform.stepDetailsForm.continuousModeDelayHelpText": "現在の時刻と最新のインプットデータ時刻の間の遅延です。", + "xpack.transform.stepDetailsForm.continuousModeDelayLabel": "遅延", + "xpack.transform.stepDetailsForm.continuousModeError": "日付フィールドがないインデックスでは、連続モードを使用できません。", + "xpack.transform.stepDetailsForm.destinationIndexHelpText": "この名前のインデックスが既に存在します。この変換を実行すると、デスティネーションインデックスが変更されます。", + "xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel": "固有の宛先インデックス名を選択してください。", + "xpack.transform.stepDetailsForm.destinationIndexInvalidError": "無効なデスティネーションインデックス名。", + "xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink": "インデックス名の制限に関する詳細。", + "xpack.transform.stepDetailsForm.destinationIndexLabel": "デスティネーションインデックス", + "xpack.transform.stepDetailsForm.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.stepDetailsForm.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.stepDetailsForm.errorGettingIndexNames": "既存のインデックス名の取得中にエラーが発生しました:", + "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", + "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", + "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "変換プレビューの取得中にエラーが発生しました。", + "xpack.transform.stepDetailsForm.frequencyAriaLabel": "頻度を選択してください。", + "xpack.transform.stepDetailsForm.frequencyError": "無効な頻度形式", + "xpack.transform.stepDetailsForm.frequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", + "xpack.transform.stepDetailsForm.frequencyLabel": "頻度", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldHelpText": "グローバル時間フィルターで使用するためのプライマリ時間フィールドを選択してください。", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldLabel": "時間フィールド", + "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel": "最大ページ検索サイズを選択してください。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeError": "max_page_search_sizeは10~10000の範囲の数値でなければなりません。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeHelpText": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeLabel": "最大ページ検索サイズ", + "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "時間フィルターを使用しない", + "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", + "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", + "xpack.transform.stepDetailsForm.transformDescriptionPlaceholderText": "説明(オプション)", + "xpack.transform.stepDetailsForm.transformIdExistsError": "この ID の変換が既に存在します。", + "xpack.transform.stepDetailsForm.transformIdInputAriaLabel": "固有のジョブ ID を選択してください。", + "xpack.transform.stepDetailsForm.transformIdInvalidError": "小文字のアルファベットと数字 (a-z と 0-9)、ハイフンまたはアンダーラインのみ使用でき、最初と最後を英数字にする必要があります。", + "xpack.transform.stepDetailsForm.transformIdLabel": "ジョブ ID", + "xpack.transform.stepDetailsSummary.advancedSettingsAccordionButtonContent": "高度な設定", + "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "連続モード日付フィールド", + "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "このジョブの Kibana インデックスパターンが作成されます。", + "xpack.transform.stepDetailsSummary.destinationIndexLabel": "デスティネーションインデックス", + "xpack.transform.stepDetailsSummary.frequencyLabel": "頻度", + "xpack.transform.stepDetailsSummary.indexPatternTimeFieldLabel": "Kibanaインデックスパターン時間フィールド", + "xpack.transform.stepDetailsSummary.maxPageSearchSizeLabel": "最大ページ検索サイズ", + "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "変換の説明", + "xpack.transform.stepDetailsSummary.transformIdLabel": "ジョブ ID", + "xpack.transform.tableActionLabel": "アクション", + "xpack.transform.toastText.closeModalButtonText": "閉じる", + "xpack.transform.toastText.modalTitle": "詳細を入力", + "xpack.transform.toastText.openModalButtonText": "詳細を表示", + "xpack.transform.transformForm.sizeNotationPlaceholder": "例: {example1}、{example2}、{example3}、{example4}", + "xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage": "{count}個のディスティネーションインデックス{count, plural, one {パターン} other {パターン}}を正常に削除しました。", + "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "{count}個のディスティネーション{count, plural, one {インデックス} other {インデックス}}を正常に削除しました。", + "xpack.transform.transformList.bulkDeleteModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を削除", + "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "{count} {count, plural, one {個の変換} other {個の変換}}を正常に削除しました。", + "xpack.transform.transformList.bulkStartModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を開始", + "xpack.transform.transformList.cloneActionNameText": "クローンを作成", + "xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "1 つまたは複数の変換が完了済みの一斉変換で、再度開始できません。", + "xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} は完了済みの一斉変換で、再度開始できません。", + "xpack.transform.transformList.createTransformButton": "変換の作成", + "xpack.transform.transformList.deleteActionDisabledToolTipContent": "削除するにはデータフレームジョブを停止してください。", + "xpack.transform.transformList.deleteActionNameText": "削除", + "xpack.transform.transformList.deleteBulkActionDisabledToolTipContent": "削除するには、選択された変換のうちの 1 つまたは複数を停止する必要があります。", + "xpack.transform.transformList.deleteModalCancelButton": "キャンセル", + "xpack.transform.transformList.deleteModalDeleteButton": "削除", + "xpack.transform.transformList.deleteModalTitle": "{transformId}を削除しますか?", + "xpack.transform.transformList.deleteTransformErrorMessage": "変換 {transformId} の削除中にエラーが発生しました", + "xpack.transform.transformList.deleteTransformGenericErrorMessage": "変換を削除するための API エンドポイントの呼び出し中にエラーが発生しました。", + "xpack.transform.transformList.deleteTransformSuccessMessage": "変換 {transformId} の削除リクエストが受け付けられました。", + "xpack.transform.transformList.editActionNameText": "編集", + "xpack.transform.transformList.editFlyoutCalloutDocs": "ドキュメントを表示", + "xpack.transform.transformList.editFlyoutCalloutText": "このフォームでは、変換を更新できます。更新できるプロパティのリストは、変換を作成するときに定義できるリストのサブセットです。", + "xpack.transform.transformList.editFlyoutCancelButtonText": "キャンセル", + "xpack.transform.transformList.editFlyoutFormAdvancedSettingsButtonContent": "高度な設定", + "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "説明", + "xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "ディスティネーション構成", + "xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "デスティネーションインデックス", + "xpack.transform.transformList.editFlyoutFormDestinationPipelineLabel": "パイプライン", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "スロットリングを有効にするには、毎秒入力するドキュメントの上限を設定します。", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "毎秒あたりのドキュメント", + "xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", + "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "頻度", + "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "頻度値が無効です。", + "xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大ページ検索サイズ", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "値は1以上の整数でなければなりません。", + "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "値は10~10000の範囲の整数でなければなりません。", + "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必須フィールド。", + "xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage": "値は文字列型でなければなりません。", + "xpack.transform.transformList.editFlyoutTitle": "{transformId}を編集", + "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", + "xpack.transform.transformList.editTransformGenericErrorMessage": "変換を削除するためのAPIエンドポイントの呼び出し中にエラーが発生しました。", + "xpack.transform.transformList.editTransformSuccessMessage": "変換{transformId}が更新されました。", + "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "ユーザーがディスティネーションインデックスを削除できるかどうかを確認するときにエラーが発生しました。", + "xpack.transform.transformList.refreshButtonLabel": "更新", + "xpack.transform.transformList.rowCollapse": "{transformId} の詳細を非表示", + "xpack.transform.transformList.rowExpand": "{transformId} の詳細を表示", + "xpack.transform.transformList.showDetailsColumn.screenReaderDescription": "このカラムには変換ごとの詳細を示すクリック可能なコントロールが含まれます", + "xpack.transform.transformList.startActionNameText": "開始", + "xpack.transform.transformList.startedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", + "xpack.transform.transformList.startedTransformToolTip": "{transformId} は既に開始済みです。", + "xpack.transform.transformList.startModalBody": "変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。", + "xpack.transform.transformList.startModalCancelButton": "キャンセル", + "xpack.transform.transformList.startModalStartButton": "開始", + "xpack.transform.transformList.startModalTitle": "{transformId}を開始しますか?", + "xpack.transform.transformList.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました", + "xpack.transform.transformList.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", + "xpack.transform.transformList.stopActionNameText": "終了", + "xpack.transform.transformList.stoppedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", + "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} は既に停止済みです。", + "xpack.transform.transformList.stopTransformErrorMessage": "データフレーム変換 {transformId} の停止中にエラーが発生しました", + "xpack.transform.transformList.stopTransformResponseSchemaErrorMessage": "変換停止要求の呼び出し中にエラーが発生しました。", + "xpack.transform.transformList.stopTransformSuccessMessage": "データフレーム変換 {transformId} の停止リクエストが受け付けられました。", + "xpack.transform.transformList.transformDescription": "変換を使用して、集約されたインデックスまたはエンティティ中心のインデックスに、既存のElasticsearchインデックスをインデックスします。", + "xpack.transform.transformList.transformDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", + "xpack.transform.transformList.transformDetails.messagesPane.messageLabel": "メッセージ", + "xpack.transform.transformList.transformDetails.messagesPane.nodeLabel": "ノード", + "xpack.transform.transformList.transformDetails.messagesPane.timeLabel": "時間", + "xpack.transform.transformList.transformDetails.tabs.transformDetailsLabel": "詳細", + "xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel": "メッセージ", + "xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel": "プレビュー", + "xpack.transform.transformList.transformDetails.tabs.transformStatsLabel": "統計", + "xpack.transform.transformList.transformDocsLinkText": "変換ドキュメント", + "xpack.transform.transformList.transformTitle": "データフレームジョブ", + "xpack.transform.transformsDescription": "変換を使用して、集約されたインデックスまたはエンティティ中心のインデックスに、既存のElasticsearchインデックスをインデックスします。", + "xpack.transform.transformsTitle": "変換", + "xpack.transform.transformsWizard.cloneTransformTitle": "クローン変換", + "xpack.transform.transformsWizard.createTransformTitle": "変換の作成", + "xpack.transform.transformsWizard.stepConfigurationTitle": "構成", + "xpack.transform.transformsWizard.stepCreateTitle": "作成", + "xpack.transform.transformsWizard.stepDetailsTitle": "ジョブの詳細", + "xpack.transform.transformsWizard.transformDocsLinkText": "変換ドキュメント", + "xpack.transform.wizard.nextStepButton": "次へ", + "xpack.transform.wizard.previousStepButton": "前へ", "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "ベータ", "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "このアクションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャルGA機能のSLAが適用されません。バグを報告したり、その他のフィードバックを提供したりして、当社を支援してください。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "変更", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d069d43de7404..d1ab2518c9ecd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -240,7 +240,6 @@ "apmOss.tutorial.startServer.title": "启动 APM Server", "apmOss.tutorial.windowsServerInstructions.textPost": "注意:如果您的系统禁用了脚本执行,则需要为当前会话设置执行策略,以允许脚本运行。示例:{command}。", "apmOss.tutorial.windowsServerInstructions.textPre": "1.从[下载页面]({downloadPageLink})下载 APM Server Windows zip 文件。\n2.将 zip 文件的内容解压缩到 {zipFileExtractFolder}。\n3.将 {apmServerDirectory} 目录重命名为 `APM-Server`。\n4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n5.从 PowerShell 提示符处,运行以下命令以将 APM Server 安装为 Windows 服务:", - "charts.advancedSettings.visualization.colorMappingText": "将值映射到可视化内的指定颜色", "charts.advancedSettings.visualization.colorMappingTitle": "颜色映射", "charts.colormaps.bluesText": "蓝色", "charts.colormaps.greensText": "绿色", @@ -4964,7 +4963,6 @@ "xpack.apm.metadataTable.section.urlLabel": "URL", "xpack.apm.metadataTable.section.userAgentLabel": "用户代理", "xpack.apm.metadataTable.section.userLabel": "用户", - "xpack.apm.metrics.plot.noDataLabel": "此时间范围内没有数据。", "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "环绕平均持续时间的流显示预期边界。对 ≥ 75 的异常分数显示标注。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时,Machine Learning 结果处于隐藏状态", @@ -5083,7 +5081,6 @@ "xpack.apm.servicesTable.transactionsPerMinuteColumnLabel": "每分钟事务数", "xpack.apm.servicesTable.transactionsPerMinuteUnitLabel": "tpm", "xpack.apm.servicesTable.UpgradeAssistantLink": "通过访问 Kibana 升级助手来了解详情", - "xpack.apm.serviceVersion": "服务版本", "xpack.apm.settings.agentConfig": "代理配置", "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "我们在以前的集成中发现 APM 应用中不再使用的旧版 Machine Learning 作业", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "复查作业", @@ -5189,9 +5186,7 @@ "xpack.apm.transactionActionMenu.actionsButtonLabel": "操作", "xpack.apm.transactionActionMenu.container.subtitle": "查看此容器的日志和指标以获取进一步详情。", "xpack.apm.transactionActionMenu.container.title": "容器详情", - "xpack.apm.transactionActionMenu.customLink.popover.title": "定制链接", "xpack.apm.transactionActionMenu.customLink.section": "定制链接", - "xpack.apm.transactionActionMenu.customLink.seeMore": "查看更多内容", "xpack.apm.transactionActionMenu.customLink.subtitle": "链接将在新窗口打开。", "xpack.apm.transactionActionMenu.host.subtitle": "查看主机日志和指标以获取进一步详情。", "xpack.apm.transactionActionMenu.host.title": "主机详情", @@ -5292,7 +5287,6 @@ "xpack.apm.ux.title": "用户体验", "xpack.apm.ux.url.hitEnter.include": "单击 {icon} 可包括与 {searchValue} 匹配的所有 URL", "xpack.apm.ux.visitorBreakdown.noData": "无数据。", - "xpack.apm.version": "版本", "xpack.apm.waterfall.exceedsMax": "此跟踪中的项目数超过显示的项目数", "xpack.beatsManagement.beat.actionSectionTypeLabel": "类型:{beatType}。", "xpack.beatsManagement.beat.actionSectionVersionLabel": "版本:{beatVersion}。", @@ -7182,17 +7176,13 @@ "xpack.fleet.agentDetails.agentVersionLabel": "代理版本", "xpack.fleet.agentDetails.hostIdLabel": "代理 ID", "xpack.fleet.agentDetails.hostNameLabel": "主机名", - "xpack.fleet.agentDetails.localMetadataSectionSubtitle": "本地元数据", - "xpack.fleet.agentDetails.metadataSectionTitle": "元数据", "xpack.fleet.agentDetails.platformLabel": "平台", "xpack.fleet.agentDetails.policyLabel": "策略", "xpack.fleet.agentDetails.releaseLabel": "代理发行版", "xpack.fleet.agentDetails.statusLabel": "状态", - "xpack.fleet.agentDetails.subTabs.activityLogTab": "活动日志", "xpack.fleet.agentDetails.subTabs.detailsTab": "代理详情", "xpack.fleet.agentDetails.unexceptedErrorTitle": "加载代理时出错", "xpack.fleet.agentDetails.upgradeAvailableTooltip": "升级可用", - "xpack.fleet.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据", "xpack.fleet.agentDetails.versionLabel": "代理版本", "xpack.fleet.agentDetails.viewAgentListTitle": "查看所有代理", "xpack.fleet.agentEnrollment.agentDescription": "将 Elastic 代理添加到您的主机,以收集数据并将其发送到 Elastic Stack。", @@ -7220,32 +7210,6 @@ "xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle": "注册并启动 Elastic 代理", "xpack.fleet.agentEnrollment.stepRunAgentDescription": "从代理目录运行此命令,以安装、注册并启动 Elastic 代理。您可以重复使用此命令在多个主机上设置代理。需要管理员权限。", "xpack.fleet.agentEnrollment.stepRunAgentTitle": "启动代理", - "xpack.fleet.agentEventsList.collapseDetailsAriaLabel": "隐藏详情", - "xpack.fleet.agentEventsList.expandDetailsAriaLabel": "显示详情", - "xpack.fleet.agentEventsList.messageColumnTitle": "消息", - "xpack.fleet.agentEventsList.messageDetailsTitle": "消息", - "xpack.fleet.agentEventsList.payloadDetailsTitle": "负载", - "xpack.fleet.agentEventsList.refreshButton": "刷新", - "xpack.fleet.agentEventsList.searchPlaceholderText": "搜索活动日志", - "xpack.fleet.agentEventsList.subtypeColumnTitle": "子类型", - "xpack.fleet.agentEventsList.timestampColumnTitle": "时间戳", - "xpack.fleet.agentEventsList.typeColumnTitle": "类型", - "xpack.fleet.agentEventSubtype.acknowledgedLabel": "已确认", - "xpack.fleet.agentEventSubtype.dataDumpLabel": "数据转储", - "xpack.fleet.agentEventSubtype.degradedLabel": "已降级", - "xpack.fleet.agentEventSubtype.failedLabel": "失败", - "xpack.fleet.agentEventSubtype.inProgressLabel": "进行中", - "xpack.fleet.agentEventSubtype.policyLabel": "策略", - "xpack.fleet.agentEventSubtype.runningLabel": "正在运行", - "xpack.fleet.agentEventSubtype.startingLabel": "正在启动", - "xpack.fleet.agentEventSubtype.stoppedLabel": "已停止", - "xpack.fleet.agentEventSubtype.stoppingLabel": "正在停止", - "xpack.fleet.agentEventSubtype.unknownLabel": "未知", - "xpack.fleet.agentEventSubtype.updatingLabel": "正在更新", - "xpack.fleet.agentEventType.actionLabel": "操作", - "xpack.fleet.agentEventType.actionResultLabel": "操作结果", - "xpack.fleet.agentEventType.errorLabel": "错误", - "xpack.fleet.agentEventType.stateLabel": "状态", "xpack.fleet.agentHealth.checkInTooltipText": "上次签入时间 {lastCheckIn}", "xpack.fleet.agentHealth.degradedStatusText": "已降级", "xpack.fleet.agentHealth.enrollingStatusText": "正在注册", @@ -7593,10 +7557,6 @@ "xpack.fleet.invalidLicenseTitle": "已过期许可证", "xpack.fleet.listTabs.agentTitle": "代理", "xpack.fleet.listTabs.enrollmentTokensTitle": "注册令牌", - "xpack.fleet.metadataForm.addButton": "+ 添加元数据", - "xpack.fleet.metadataForm.keyLabel": "键", - "xpack.fleet.metadataForm.submitButtonText": "添加", - "xpack.fleet.metadataForm.valueLabel": "值", "xpack.fleet.namespaceValidation.invalidCharactersErrorMessage": "命名空间包含无效字符", "xpack.fleet.namespaceValidation.lowercaseErrorMessage": "命名空间必须小写", "xpack.fleet.namespaceValidation.requiredErrorMessage": "“命名空间”必填", @@ -19513,307 +19473,85 @@ "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "用作阈值的值数组;“between”和“notBetween”需要两个值,其他则需要一个值。", "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "告警的预构造标题。", "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "超过阈值的值。", - "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "告警 {name} 组 {group} 值 {value} 在 {window} 于 {date}超过了阈值 {function}", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 超过了阈值", "xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值", + "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", + "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", + "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "选择边界名称", + "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", + "xpack.stackAlerts.geoThreshold.delayOffset": "已延迟的评估偏移", + "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", + "xpack.stackAlerts.geoThreshold.entityByLabel": "方式", + "xpack.stackAlerts.geoThreshold.entityIndexLabel": "索引", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "“实体”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", + "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空间字段", + "xpack.stackAlerts.geoThreshold.indexLabel": "索引", + "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "索引模式", + "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", + "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "跟踪阈值", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", + "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "选择边界:", + "xpack.stackAlerts.geoThreshold.selectEntity": "选择实体", + "xpack.stackAlerts.geoThreshold.selectGeoLabel": "选择地理字段", + "xpack.stackAlerts.geoThreshold.selectIndex": "定义条件", + "xpack.stackAlerts.geoThreshold.selectLabel": "选择地理字段", + "xpack.stackAlerts.geoThreshold.selectOffset": "选择偏移(可选)", + "xpack.stackAlerts.geoThreshold.selectTimeLabel": "选择时间字段", + "xpack.stackAlerts.geoThreshold.timeFieldLabel": "时间字段", + "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", + "xpack.stackAlerts.geoThreshold.whenEntityLabel": "当实体", + "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "聚合字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "“索引”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "“词大小”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "阈值 0 必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "阈值 1 必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "时间字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "词字段必填。", + "xpack.stackAlerts.threshold.ui.conditionPrompt": "定义条件", + "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", + "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "关闭", + "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……", + "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "完成表达式以生成预览。", + "xpack.stackAlerts.threshold.ui.selectIndex": "选择索引", + "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "关闭", + "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", + "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "索引", + "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "索引", + "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "要查询的索引", + "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "时间字段", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", + "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "无法加载可视化", + "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", "xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]", "xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", "xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", "xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”", - "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", "xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "日期 {date} 无效", "xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "持续时间无效:“{duration}”", "xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”", "xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", - "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", "xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”", "xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", "xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,termField 为必需", "xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,termSize 为必需", - "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", - "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", - "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", - "xpack.transform.actionDeleteTransform.deleteDestIndexPatternTitle": "删除索引模式 {destinationIndex}", - "xpack.transform.agg.popoverForm.aggLabel": "聚合", - "xpack.transform.agg.popoverForm.aggNameAlreadyUsedError": "其他聚合已使用该名称。", - "xpack.transform.agg.popoverForm.aggNameInvalidCharError": "名称无效。不允许使用字符“[”、“]”和“>”,且名称不得以空格字符开头或结束。", - "xpack.transform.agg.popoverForm.fieldLabel": "字段", - "xpack.transform.agg.popoverForm.filerAgg.range.greaterThanLabel": "大于", - "xpack.transform.agg.popoverForm.filerAgg.range.lessThanLabel": "小于", - "xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions": "无法获取建议", - "xpack.transform.agg.popoverForm.filerAgg.term.valueLabel": "值", - "xpack.transform.agg.popoverForm.filerAggLabel": "筛选查询", - "xpack.transform.agg.popoverForm.nameLabel": "聚合名称", - "xpack.transform.agg.popoverForm.percentsLabel": "百分数", - "xpack.transform.agg.popoverForm.submitButtonLabel": "应用", - "xpack.transform.agg.popoverForm.unsupportedAggregationHelpText": "在此表单中仅可以编辑聚合名称。请使用高级编辑器编辑聚合的其他部分。", - "xpack.transform.aggLabelForm.deleteItemAriaLabel": "删除项", - "xpack.transform.aggLabelForm.editAggAriaLabel": "编辑聚合", - "xpack.transform.app.checkingPrivilegesDescription": "正在检查权限……", - "xpack.transform.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错。", - "xpack.transform.app.deniedPrivilegeDescription": "要使用“转换”的此部分,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", - "xpack.transform.app.deniedPrivilegeTitle": "您缺少集群权限", - "xpack.transform.appName": "数据帧作业", - "xpack.transform.appTitle": "转换", - "xpack.transform.capability.noPermission.createTransformTooltip": "您无权创建数据帧转换。", - "xpack.transform.capability.noPermission.deleteTransformTooltip": "您无权删除数据帧转换。", - "xpack.transform.capability.noPermission.startOrStopTransformTooltip": "您无权启动或停止转换。", - "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message}请联系您的管理员。", - "xpack.transform.clone.errorPromptText": "检查源索引模式是否存在时发生错误", - "xpack.transform.clone.errorPromptTitle": "获取转换配置时发生错误。", - "xpack.transform.clone.fetchErrorPromptText": "无法提取 Kibana 索引模式 ID。", - "xpack.transform.clone.noIndexPatternErrorPromptText": "无法克隆转换。对于 {indexPattern},不存在索引模式。", - "xpack.transform.cloneTransform.breadcrumbTitle": "克隆转换", - "xpack.transform.createTransform.breadcrumbTitle": "创建转换", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage": "删除索引模式 {destinationIndex} 时发生错误", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage": "删除索引模式 {destinationIndex} 的请求已确认。", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", - "xpack.transform.deleteTransform.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}", - "xpack.transform.description": "描述", - "xpack.transform.groupby.popoverForm.aggLabel": "聚合", - "xpack.transform.groupBy.popoverForm.aggNameAlreadyUsedError": "其他分组依据配置已使用该名称。", - "xpack.transform.groupBy.popoverForm.aggNameInvalidCharError": "名称无效。不允许使用字符“[”、“]”和“>”,且名称不得以空格字符开头或结束。", - "xpack.transform.groupBy.popoverForm.fieldLabel": "字段", - "xpack.transform.groupBy.popoverForm.intervalError": "时间间隔无效。", - "xpack.transform.groupBy.popoverForm.intervalLabel": "时间间隔", - "xpack.transform.groupBy.popoverForm.intervalPercents": "输入百分位数的逗号分隔列表", - "xpack.transform.groupBy.popoverForm.nameLabel": "分组依据名称", - "xpack.transform.groupBy.popoverForm.submitButtonLabel": "应用", - "xpack.transform.groupBy.popoverForm.unsupportedGroupByHelpText": "在此表单中仅可以编辑 group_by 名称。请使用高级编辑器编辑 group_by 配置的其他部分。", - "xpack.transform.groupByLabelForm.deleteItemAriaLabel": "删除项", - "xpack.transform.groupByLabelForm.editIntervalAriaLabel": "编辑时间间隔", - "xpack.transform.home.breadcrumbTitle": "数据帧作业", - "xpack.transform.indexPreview.copyClipboardTooltip": "将索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.licenseCheckErrorMessage": "许可证检查失败", - "xpack.transform.list.emptyPromptButtonText": "创建您的首个转换", - "xpack.transform.list.emptyPromptTitle": "找不到转换", - "xpack.transform.list.errorPromptTitle": "获取数据帧转换列表时发生错误。", - "xpack.transform.mode": "模式", - "xpack.transform.modeFilter": "模式", - "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "所有其他请求已取消。", - "xpack.transform.models.transformService.requestToActionTimedOutErrorMessage": "对 {action}“{id}”的请求超时。{extra}", - "xpack.transform.multiTransformActionsMenu.managementActionsAriaLabel": "管理操作", - "xpack.transform.multiTransformActionsMenu.transformsCount": "已选择 {count} 个{count, plural, one {转换} other {转换}}", - "xpack.transform.newTransform.chooseSourceTitle": "选择源", - "xpack.transform.newTransform.newTransformTitle": "新转换", - "xpack.transform.newTransform.searchSelection.notFoundLabel": "未找到匹配的索引或已保存搜索。", - "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", - "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", - "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", - "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", - "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", - "xpack.transform.progress": "进度", - "xpack.transform.statsBar.batchTransformsLabel": "批量", - "xpack.transform.statsBar.continuousTransformsLabel": "连续", - "xpack.transform.statsBar.failedTransformsLabel": "失败", - "xpack.transform.statsBar.startedTransformsLabel": "已启动", - "xpack.transform.statsBar.totalTransformsLabel": "转换总数", - "xpack.transform.status": "状态", - "xpack.transform.statusFilter": "状态", - "xpack.transform.stepCreateForm.continuousModeLabel": "连续模式", - "xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton": "复制到剪贴板", - "xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription": "将用于创建作业的 Kibana 开发控制台命令复制到剪贴板。", - "xpack.transform.stepCreateForm.createAndStartTransformButton": "创建并启动", - "xpack.transform.stepCreateForm.createAndStartTransformDescription": "创建并启动转换。转换将增加集群的搜索和索引负荷。如果负荷超载,请停止转换。转换启动后,系统将为您提供继续浏览转换的选项。", - "xpack.transform.stepCreateForm.createIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误 {indexPatternName}:", - "xpack.transform.stepCreateForm.createIndexPatternLabel": "创建索引模式", - "xpack.transform.stepCreateForm.createIndexPatternSuccessMessage": "Kibana 索引模式 {indexPatternName} 成功创建。", - "xpack.transform.stepCreateForm.createTransformButton": "创建", - "xpack.transform.stepCreateForm.createTransformDescription": "在不启动转换的情况下创建转换。您之后能够通过返回到转换列表,来启动转换。", - "xpack.transform.stepCreateForm.createTransformErrorMessage": "创建转换 {transformId} 时出错:", - "xpack.transform.stepCreateForm.createTransformSuccessMessage": "创建转换 {transformId} 的请求已确认。", - "xpack.transform.stepCreateForm.creatingIndexPatternMessage": "正在创建 Kibana 索引模式......", - "xpack.transform.stepCreateForm.discoverCardDescription": "使用 Discover 浏览数据帧透视表。", - "xpack.transform.stepCreateForm.discoverCardTitle": "Discover", - "xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误 {indexPatternName}:该索引模式已存在。", - "xpack.transform.stepCreateForm.progressErrorMessage": "获取进度百分比时出错:", - "xpack.transform.stepCreateForm.progressTitle": "进度", - "xpack.transform.stepCreateForm.startTransformButton": "开始", - "xpack.transform.stepCreateForm.startTransformDescription": "启动转换。转换将增加集群的搜索和索引负荷。如果负荷超载,请停止转换。转换启动后,系统将为您提供继续浏览转换的选项。", - "xpack.transform.stepCreateForm.startTransformErrorMessage": "启动转换 {transformId} 时发生错误:", - "xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage": "调用启动转换请求时发生错误。", - "xpack.transform.stepCreateForm.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", - "xpack.transform.stepCreateForm.transformListCardDescription": "返回数据帧作业管理页面。", - "xpack.transform.stepCreateForm.transformListCardTitle": "数据帧作业", - "xpack.transform.stepDefineForm.addSubAggregationPlaceholder": "添加子聚合......", - "xpack.transform.stepDefineForm.advancedEditorApplyButtonText": "应用更改", - "xpack.transform.stepDefineForm.advancedEditorAriaLabel": "高级数据透视表编辑器", - "xpack.transform.stepDefineForm.advancedEditorHelpText": "高级编辑器允许您编辑数据帧转换的数据透视表配置。", - "xpack.transform.stepDefineForm.advancedEditorHelpTextLink": "详细了解可用选项。", - "xpack.transform.stepDefineForm.advancedEditorLabel": "数据透视表配置对象", - "xpack.transform.stepDefineForm.advancedEditorSourceConfigSwitchLabel": "编辑 JSON 查询", - "xpack.transform.stepDefineForm.advancedEditorSwitchLabel": "编辑 JSON 配置", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalBodyText": "高级编辑器中的更改尚未应用。禁用高级编辑器将会使您的编辑丢失。", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalCancelButtonText": "取消", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalConfirmButtonText": "禁用高级编辑器", - "xpack.transform.stepDefineForm.advancedEditorSwitchModalTitle": "未应用的更改", - "xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText": "应用更改", - "xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel": "高级查询编辑器", - "xpack.transform.stepDefineForm.advancedSourceEditorHelpText": "高级编辑器允许您编辑转换配置的源查询子句。", - "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalBodyText": "切换回到查询栏,您将会丢失编辑。", - "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalConfirmButtonText": "切换至查询栏", - "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalTitle": "编辑将会丢失", - "xpack.transform.stepDefineForm.aggExistsErrorMessage": "名称为“{aggName}”的聚合配置已存在。", - "xpack.transform.stepDefineForm.aggregationsLabel": "聚合", - "xpack.transform.stepDefineForm.aggregationsPlaceholder": "添加聚合……", - "xpack.transform.stepDefineForm.groupByExistsErrorMessage": "名称为“{aggName}”的分组依据配置已存在。", - "xpack.transform.stepDefineForm.groupByLabel": "分组依据", - "xpack.transform.stepDefineForm.groupByPlaceholder": "添加分组依据字段……", - "xpack.transform.stepDefineForm.indexPatternLabel": "索引模式", - "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "查询无效:{errorMessage}", - "xpack.transform.stepDefineForm.maxSubAggsLevelsLimitMessage": "您已达到可在表单中添加的最大子聚合级别数。如果想再添加一个级别,请编辑 JSON 配置。", - "xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage": "无法添加配置“{aggName}”,因为与“{aggListName}”有嵌套冲突。", - "xpack.transform.stepDefineForm.nestedConflictErrorMessage": "无法添加配置“{aggName}”,因为与“{aggNameCheck}”有嵌套冲突。", - "xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage": "无法添加配置“{aggName}”,因为与“{groupByListName}”有嵌套冲突。", - "xpack.transform.stepDefineForm.queryPlaceholderKql": "例如,{example}", - "xpack.transform.stepDefineForm.queryPlaceholderLucene": "例如,{example}", - "xpack.transform.stepDefineForm.savedSearchLabel": "已保存搜索", - "xpack.transform.stepDefineSummary.aggregationsLabel": "聚合", - "xpack.transform.stepDefineSummary.groupByLabel": "分组依据", - "xpack.transform.stepDefineSummary.indexPatternLabel": "索引模式", - "xpack.transform.stepDefineSummary.queryCodeBlockLabel": "查询", - "xpack.transform.stepDefineSummary.queryLabel": "查询", - "xpack.transform.stepDefineSummary.savedSearchLabel": "已保存搜索", - "xpack.transform.stepDetailsForm.advancedSettingsAccordionButtonContent": "高级设置", - "xpack.transform.stepDetailsForm.continuousModeAriaLabel": "选择延迟。", - "xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText": "选择可用于标识新文档的日期字段。", - "xpack.transform.stepDetailsForm.continuousModeDateFieldLabel": "日期字段", - "xpack.transform.stepDetailsForm.continuousModeDelayError": "延迟格式无效", - "xpack.transform.stepDetailsForm.continuousModeDelayHelpText": "当前时间和最新输入数据时间之间的时间延迟。", - "xpack.transform.stepDetailsForm.continuousModeDelayLabel": "延迟", - "xpack.transform.stepDetailsForm.continuousModeError": "连续模式不可用于没有日期字段的索引。", - "xpack.transform.stepDetailsForm.destinationIndexHelpText": "已存在具有此名称的索引。请注意,运行此转换将会修改此目标索引。", - "xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel": "选择唯一目标索引名称。", - "xpack.transform.stepDetailsForm.destinationIndexInvalidError": "目标索引名称无效。", - "xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink": "详细了解索引名称限制。", - "xpack.transform.stepDetailsForm.destinationIndexLabel": "目标 IP", - "xpack.transform.stepDetailsForm.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}", - "xpack.transform.stepDetailsForm.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}", - "xpack.transform.stepDetailsForm.errorGettingIndexNames": "获取现有索引名称时发生错误:", - "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", - "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", - "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "提取转换预览时发生错误", - "xpack.transform.stepDetailsForm.frequencyAriaLabel": "选择频率。", - "xpack.transform.stepDetailsForm.frequencyError": "频率格式无效", - "xpack.transform.stepDetailsForm.frequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。", - "xpack.transform.stepDetailsForm.frequencyLabel": "频率", - "xpack.transform.stepDetailsForm.indexPatternTimeFieldHelpText": "选择用于全局时间筛选的主要时间字段。", - "xpack.transform.stepDetailsForm.indexPatternTimeFieldLabel": "时间字段", - "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel": "选择最大页面搜索大小。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeError": "max_page_search_size 必须是介于 10 到 10000 之间的数字。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeHelpText": "定义用于每个检查点的组合聚合的初始页面大小。", - "xpack.transform.stepDetailsForm.maxPageSearchSizeLabel": "最大页面搜索大小", - "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "我不想使用时间筛选", - "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", - "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", - "xpack.transform.stepDetailsForm.transformDescriptionPlaceholderText": "描述(可选)", - "xpack.transform.stepDetailsForm.transformIdExistsError": "已存在具有此 ID 的转换。", - "xpack.transform.stepDetailsForm.transformIdInputAriaLabel": "选择唯一的作业 ID。", - "xpack.transform.stepDetailsForm.transformIdInvalidError": "只能包含小写字母数字字符(a-z 和 0-9)、连字符和下划线,并且必须以字母数字字符开头和结尾。", - "xpack.transform.stepDetailsForm.transformIdLabel": "作业 ID", - "xpack.transform.stepDetailsSummary.advancedSettingsAccordionButtonContent": "高级设置", - "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "连续模式日期字段", - "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "将为此作业创建 Kibana 索引模式。", - "xpack.transform.stepDetailsSummary.destinationIndexLabel": "目标 IP", - "xpack.transform.stepDetailsSummary.frequencyLabel": "频率", - "xpack.transform.stepDetailsSummary.indexPatternTimeFieldLabel": "Kibana 索引模式时间字段", - "xpack.transform.stepDetailsSummary.maxPageSearchSizeLabel": "最大页面搜索大小", - "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "转换描述", - "xpack.transform.stepDetailsSummary.transformIdLabel": "作业 ID", - "xpack.transform.tableActionLabel": "操作", - "xpack.transform.toastText.closeModalButtonText": "关闭", - "xpack.transform.toastText.modalTitle": "错误详细信息", - "xpack.transform.toastText.openModalButtonText": "查看详情", - "xpack.transform.transformForm.sizeNotationPlaceholder": "示例:{example1}、{example2}、{example3}、{example4}", - "xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage": "已成功删除 {count} 个目标索引{count, plural, one {模式} other {模式}}。", - "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "已成功删除 {count} 个目标{count, plural, one {索引} other {索引}}。", - "xpack.transform.transformList.bulkDeleteModalTitle": "删除 {count} 个 {count, plural, one {转换} other {转换}}?", - "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "已成功删除 {count} 个{count, plural, one {转换} other {转换}}。", - "xpack.transform.transformList.bulkStartModalTitle": "启动 {count} 个 {count, plural, one {转换} other {转换}}?", - "xpack.transform.transformList.cloneActionNameText": "克隆", - "xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "一个或多个转换为已完成批量转换,无法重新启动。", - "xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} 为已完成批量转换,无法重新启动。", - "xpack.transform.transformList.createTransformButton": "创建转换", - "xpack.transform.transformList.deleteActionDisabledToolTipContent": "停止数据帧作业,以便将其删除。", - "xpack.transform.transformList.deleteActionNameText": "删除", - "xpack.transform.transformList.deleteBulkActionDisabledToolTipContent": "一个或多个选定数据帧转换必须停止,才能删除。", - "xpack.transform.transformList.deleteModalCancelButton": "取消", - "xpack.transform.transformList.deleteModalDeleteButton": "删除", - "xpack.transform.transformList.deleteModalTitle": "删除 {transformId}?", - "xpack.transform.transformList.deleteTransformErrorMessage": "删除转换 {transformId} 时发生错误", - "xpack.transform.transformList.deleteTransformGenericErrorMessage": "调用用于删除转换的 API 终端节点时发生错误。", - "xpack.transform.transformList.deleteTransformSuccessMessage": "删除转换 {transformId} 的请求已确认。", - "xpack.transform.transformList.editActionNameText": "编辑", - "xpack.transform.transformList.editFlyoutCalloutDocs": "查看文档", - "xpack.transform.transformList.editFlyoutCalloutText": "此表单允许您更新转换。可以更新的属性列表是创建转换时可以定义的列表子集。", - "xpack.transform.transformList.editFlyoutCancelButtonText": "取消", - "xpack.transform.transformList.editFlyoutFormAdvancedSettingsButtonContent": "高级设置", - "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "描述", - "xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "目标配置", - "xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "目标索引", - "xpack.transform.transformList.editFlyoutFormDestinationPipelineLabel": "管道", - "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "要启用节流,请设置每秒要输入的文档限值。", - "xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "每秒文档数", - "xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。", - "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "频率", - "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "频率值无效。", - "xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}", - "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "定义用于每个检查点的组合聚合的初始页面大小。", - "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大页面搜索大小", - "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}", - "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "值必须是大于零的整数。", - "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "值必须是介于 10 到 10000 之间的整数。", - "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必填字段。", - "xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage": "值需要为字符串类型。", - "xpack.transform.transformList.editFlyoutTitle": "编辑 {transformId}", - "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", - "xpack.transform.transformList.editTransformGenericErrorMessage": "调用用于更新转换的 API 终端时发生错误。", - "xpack.transform.transformList.editTransformSuccessMessage": "转换 {transformId} 已更新。", - "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "检查用户是否可以删除目标索引时发生错误", - "xpack.transform.transformList.refreshButtonLabel": "刷新", - "xpack.transform.transformList.rowCollapse": "隐藏 {transformId} 的详情", - "xpack.transform.transformList.rowExpand": "显示 {transformId} 的详情", - "xpack.transform.transformList.showDetailsColumn.screenReaderDescription": "此列包含可单击控件,用于显示每个转换的更多详情", - "xpack.transform.transformList.startActionNameText": "启动", - "xpack.transform.transformList.startedTransformBulkToolTip": "一个或多个选定数据帧转换已启动。", - "xpack.transform.transformList.startedTransformToolTip": "{transformId} 已启动。", - "xpack.transform.transformList.startModalBody": "转换将增加集群的搜索和索引负载。如果超负荷,请停止转换。", - "xpack.transform.transformList.startModalCancelButton": "取消", - "xpack.transform.transformList.startModalStartButton": "启动", - "xpack.transform.transformList.startModalTitle": "启动 {transformId}?", - "xpack.transform.transformList.startTransformErrorMessage": "启动转换 {transformId} 时发生错误", - "xpack.transform.transformList.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", - "xpack.transform.transformList.stopActionNameText": "停止", - "xpack.transform.transformList.stoppedTransformBulkToolTip": "一个或多个选定数据帧转换已停止。", - "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} 已停止。", - "xpack.transform.transformList.stopTransformErrorMessage": "停止数据帧转换 {transformId} 时发生错误", - "xpack.transform.transformList.stopTransformResponseSchemaErrorMessage": "调用停止转换请求时发生错误。", - "xpack.transform.transformList.stopTransformSuccessMessage": "停止数据帧转换 {transformId} 的请求已确认。", - "xpack.transform.transformList.transformDescription": "使用转换将现有 Elasticsearch 索引切换到摘要式或以实体为中心的索引。", - "xpack.transform.transformList.transformDetails.messagesPane.errorMessage": "无法加载消息", - "xpack.transform.transformList.transformDetails.messagesPane.messageLabel": "消息", - "xpack.transform.transformList.transformDetails.messagesPane.nodeLabel": "节点", - "xpack.transform.transformList.transformDetails.messagesPane.timeLabel": "时间", - "xpack.transform.transformList.transformDetails.tabs.transformDetailsLabel": "详情", - "xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel": "消息", - "xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel": "预览", - "xpack.transform.transformList.transformDetails.tabs.transformStatsLabel": "统计", - "xpack.transform.transformList.transformDocsLinkText": "转换文档", - "xpack.transform.transformList.transformTitle": "数据帧作业", - "xpack.transform.transformsDescription": "使用转换将现有 Elasticsearch 索引透视成摘要式或以实体为中心的索引。", - "xpack.transform.transformsTitle": "转换", - "xpack.transform.transformsWizard.cloneTransformTitle": "克隆转换", - "xpack.transform.transformsWizard.createTransformTitle": "创建转换", - "xpack.transform.transformsWizard.stepConfigurationTitle": "配置", - "xpack.transform.transformsWizard.stepCreateTitle": "创建", - "xpack.transform.transformsWizard.stepDetailsTitle": "作业详情", - "xpack.transform.transformsWizard.transformDocsLinkText": "转换文档", - "xpack.transform.wizard.nextStepButton": "下一个", - "xpack.transform.wizard.previousStepButton": "上一页", "xpack.triggersActionsUI.actionVariables.alertIdLabel": "告警的 ID。", "xpack.triggersActionsUI.actionVariables.alertInstanceIdLabel": "为告警排定操作的告警实例 ID。", "xpack.triggersActionsUI.actionVariables.alertNameLabel": "告警的名称。", @@ -20066,42 +19804,6 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "无法恢复{numIdsToDelete, plural, one {删除的{singleTitle}} other {删除的{multipleTitle}}}。", - "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "选择边界名称", - "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", - "xpack.stackAlerts.geoThreshold.delayOffset": "已延迟的评估偏移", - "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", - "xpack.stackAlerts.geoThreshold.entityByLabel": "方式", - "xpack.stackAlerts.geoThreshold.entityIndexLabel": "索引", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", - "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "“实体”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", - "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", - "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空间字段", - "xpack.stackAlerts.geoThreshold.indexLabel": "索引", - "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "索引模式", - "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", - "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "跟踪阈值", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", - "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", - "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", - "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", - "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "选择边界:", - "xpack.stackAlerts.geoThreshold.selectEntity": "选择实体", - "xpack.stackAlerts.geoThreshold.selectGeoLabel": "选择地理字段", - "xpack.stackAlerts.geoThreshold.selectIndex": "定义条件", - "xpack.stackAlerts.geoThreshold.selectLabel": "选择地理字段", - "xpack.stackAlerts.geoThreshold.selectOffset": "选择偏移(可选)", - "xpack.stackAlerts.geoThreshold.selectTimeLabel": "选择时间字段", - "xpack.stackAlerts.geoThreshold.timeFieldLabel": "时间字段", - "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", - "xpack.stackAlerts.geoThreshold.whenEntityLabel": "当实体", "xpack.triggersActionsUI.home.alertsTabTitle": "告警", "xpack.triggersActionsUI.home.appTitle": "告警和操作", "xpack.triggersActionsUI.home.breadcrumbTitle": "告警和操作", @@ -20145,15 +19847,6 @@ "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "“值”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "“密码”必填。", - "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "聚合字段必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "“索引”必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "“词大小”必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "阈值 0 必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "阈值 1 必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "时间字段必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", - "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "词字段必填。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。", @@ -20162,27 +19855,11 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.stackAlerts.threshold.ui.conditionPrompt": "定义条件", - "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", - "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "关闭", - "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……", "xpack.triggersActionsUI.sections.alertAdd.operationName": "创建", - "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "完成表达式以生成预览。", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", - "xpack.stackAlerts.threshold.ui.selectIndex": "选择索引", - "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "关闭", - "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", - "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "索引", - "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "索引", - "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "要查询的索引", - "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "时间字段", "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "选择字段", - "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", - "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", - "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "无法加载可视化", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "此告警已禁用,无法显示。切换禁用 ↑ 以激活。", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "持续时间", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "实例", @@ -20335,6 +20012,289 @@ "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, one {秒} other {秒}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "未注册对象类型“{id}”。", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "已注册对象类型“{id}”。", + "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", + "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", + "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", + "xpack.transform.actionDeleteTransform.deleteDestIndexPatternTitle": "删除索引模式 {destinationIndex}", + "xpack.transform.agg.popoverForm.aggLabel": "聚合", + "xpack.transform.agg.popoverForm.aggNameAlreadyUsedError": "其他聚合已使用该名称。", + "xpack.transform.agg.popoverForm.aggNameInvalidCharError": "名称无效。不允许使用字符“[”、“]”和“>”,且名称不得以空格字符开头或结束。", + "xpack.transform.agg.popoverForm.fieldLabel": "字段", + "xpack.transform.agg.popoverForm.filerAgg.range.greaterThanLabel": "大于", + "xpack.transform.agg.popoverForm.filerAgg.range.lessThanLabel": "小于", + "xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions": "无法获取建议", + "xpack.transform.agg.popoverForm.filerAgg.term.valueLabel": "值", + "xpack.transform.agg.popoverForm.filerAggLabel": "筛选查询", + "xpack.transform.agg.popoverForm.nameLabel": "聚合名称", + "xpack.transform.agg.popoverForm.percentsLabel": "百分数", + "xpack.transform.agg.popoverForm.submitButtonLabel": "应用", + "xpack.transform.agg.popoverForm.unsupportedAggregationHelpText": "在此表单中仅可以编辑聚合名称。请使用高级编辑器编辑聚合的其他部分。", + "xpack.transform.aggLabelForm.deleteItemAriaLabel": "删除项", + "xpack.transform.aggLabelForm.editAggAriaLabel": "编辑聚合", + "xpack.transform.app.checkingPrivilegesDescription": "正在检查权限……", + "xpack.transform.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错。", + "xpack.transform.app.deniedPrivilegeDescription": "要使用“转换”的此部分,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", + "xpack.transform.app.deniedPrivilegeTitle": "您缺少集群权限", + "xpack.transform.appName": "数据帧作业", + "xpack.transform.appTitle": "转换", + "xpack.transform.capability.noPermission.createTransformTooltip": "您无权创建数据帧转换。", + "xpack.transform.capability.noPermission.deleteTransformTooltip": "您无权删除数据帧转换。", + "xpack.transform.capability.noPermission.startOrStopTransformTooltip": "您无权启动或停止转换。", + "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message}请联系您的管理员。", + "xpack.transform.clone.errorPromptText": "检查源索引模式是否存在时发生错误", + "xpack.transform.clone.errorPromptTitle": "获取转换配置时发生错误。", + "xpack.transform.clone.fetchErrorPromptText": "无法提取 Kibana 索引模式 ID。", + "xpack.transform.clone.noIndexPatternErrorPromptText": "无法克隆转换。对于 {indexPattern},不存在索引模式。", + "xpack.transform.cloneTransform.breadcrumbTitle": "克隆转换", + "xpack.transform.createTransform.breadcrumbTitle": "创建转换", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage": "删除索引模式 {destinationIndex} 时发生错误", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage": "删除索引模式 {destinationIndex} 的请求已确认。", + "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", + "xpack.transform.deleteTransform.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}", + "xpack.transform.description": "描述", + "xpack.transform.groupby.popoverForm.aggLabel": "聚合", + "xpack.transform.groupBy.popoverForm.aggNameAlreadyUsedError": "其他分组依据配置已使用该名称。", + "xpack.transform.groupBy.popoverForm.aggNameInvalidCharError": "名称无效。不允许使用字符“[”、“]”和“>”,且名称不得以空格字符开头或结束。", + "xpack.transform.groupBy.popoverForm.fieldLabel": "字段", + "xpack.transform.groupBy.popoverForm.intervalError": "时间间隔无效。", + "xpack.transform.groupBy.popoverForm.intervalLabel": "时间间隔", + "xpack.transform.groupBy.popoverForm.intervalPercents": "输入百分位数的逗号分隔列表", + "xpack.transform.groupBy.popoverForm.nameLabel": "分组依据名称", + "xpack.transform.groupBy.popoverForm.submitButtonLabel": "应用", + "xpack.transform.groupBy.popoverForm.unsupportedGroupByHelpText": "在此表单中仅可以编辑 group_by 名称。请使用高级编辑器编辑 group_by 配置的其他部分。", + "xpack.transform.groupByLabelForm.deleteItemAriaLabel": "删除项", + "xpack.transform.groupByLabelForm.editIntervalAriaLabel": "编辑时间间隔", + "xpack.transform.home.breadcrumbTitle": "数据帧作业", + "xpack.transform.indexPreview.copyClipboardTooltip": "将索引预览的开发控制台语句复制到剪贴板。", + "xpack.transform.licenseCheckErrorMessage": "许可证检查失败", + "xpack.transform.list.emptyPromptButtonText": "创建您的首个转换", + "xpack.transform.list.emptyPromptTitle": "找不到转换", + "xpack.transform.list.errorPromptTitle": "获取数据帧转换列表时发生错误。", + "xpack.transform.mode": "模式", + "xpack.transform.modeFilter": "模式", + "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "所有其他请求已取消。", + "xpack.transform.models.transformService.requestToActionTimedOutErrorMessage": "对 {action}“{id}”的请求超时。{extra}", + "xpack.transform.multiTransformActionsMenu.managementActionsAriaLabel": "管理操作", + "xpack.transform.multiTransformActionsMenu.transformsCount": "已选择 {count} 个{count, plural, one {转换} other {转换}}", + "xpack.transform.newTransform.chooseSourceTitle": "选择源", + "xpack.transform.newTransform.newTransformTitle": "新转换", + "xpack.transform.newTransform.searchSelection.notFoundLabel": "未找到匹配的索引或已保存搜索。", + "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", + "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", + "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", + "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", + "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", + "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", + "xpack.transform.progress": "进度", + "xpack.transform.statsBar.batchTransformsLabel": "批量", + "xpack.transform.statsBar.continuousTransformsLabel": "连续", + "xpack.transform.statsBar.failedTransformsLabel": "失败", + "xpack.transform.statsBar.startedTransformsLabel": "已启动", + "xpack.transform.statsBar.totalTransformsLabel": "转换总数", + "xpack.transform.status": "状态", + "xpack.transform.statusFilter": "状态", + "xpack.transform.stepCreateForm.continuousModeLabel": "连续模式", + "xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton": "复制到剪贴板", + "xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription": "将用于创建作业的 Kibana 开发控制台命令复制到剪贴板。", + "xpack.transform.stepCreateForm.createAndStartTransformButton": "创建并启动", + "xpack.transform.stepCreateForm.createAndStartTransformDescription": "创建并启动转换。转换将增加集群的搜索和索引负荷。如果负荷超载,请停止转换。转换启动后,系统将为您提供继续浏览转换的选项。", + "xpack.transform.stepCreateForm.createIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误 {indexPatternName}:", + "xpack.transform.stepCreateForm.createIndexPatternLabel": "创建索引模式", + "xpack.transform.stepCreateForm.createIndexPatternSuccessMessage": "Kibana 索引模式 {indexPatternName} 成功创建。", + "xpack.transform.stepCreateForm.createTransformButton": "创建", + "xpack.transform.stepCreateForm.createTransformDescription": "在不启动转换的情况下创建转换。您之后能够通过返回到转换列表,来启动转换。", + "xpack.transform.stepCreateForm.createTransformErrorMessage": "创建转换 {transformId} 时出错:", + "xpack.transform.stepCreateForm.createTransformSuccessMessage": "创建转换 {transformId} 的请求已确认。", + "xpack.transform.stepCreateForm.creatingIndexPatternMessage": "正在创建 Kibana 索引模式......", + "xpack.transform.stepCreateForm.discoverCardDescription": "使用 Discover 浏览数据帧透视表。", + "xpack.transform.stepCreateForm.discoverCardTitle": "Discover", + "xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误 {indexPatternName}:该索引模式已存在。", + "xpack.transform.stepCreateForm.progressErrorMessage": "获取进度百分比时出错:", + "xpack.transform.stepCreateForm.progressTitle": "进度", + "xpack.transform.stepCreateForm.startTransformButton": "开始", + "xpack.transform.stepCreateForm.startTransformDescription": "启动转换。转换将增加集群的搜索和索引负荷。如果负荷超载,请停止转换。转换启动后,系统将为您提供继续浏览转换的选项。", + "xpack.transform.stepCreateForm.startTransformErrorMessage": "启动转换 {transformId} 时发生错误:", + "xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage": "调用启动转换请求时发生错误。", + "xpack.transform.stepCreateForm.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", + "xpack.transform.stepCreateForm.transformListCardDescription": "返回数据帧作业管理页面。", + "xpack.transform.stepCreateForm.transformListCardTitle": "数据帧作业", + "xpack.transform.stepDefineForm.addSubAggregationPlaceholder": "添加子聚合......", + "xpack.transform.stepDefineForm.advancedEditorApplyButtonText": "应用更改", + "xpack.transform.stepDefineForm.advancedEditorAriaLabel": "高级数据透视表编辑器", + "xpack.transform.stepDefineForm.advancedEditorHelpText": "高级编辑器允许您编辑数据帧转换的数据透视表配置。", + "xpack.transform.stepDefineForm.advancedEditorHelpTextLink": "详细了解可用选项。", + "xpack.transform.stepDefineForm.advancedEditorLabel": "数据透视表配置对象", + "xpack.transform.stepDefineForm.advancedEditorSourceConfigSwitchLabel": "编辑 JSON 查询", + "xpack.transform.stepDefineForm.advancedEditorSwitchLabel": "编辑 JSON 配置", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalBodyText": "高级编辑器中的更改尚未应用。禁用高级编辑器将会使您的编辑丢失。", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalCancelButtonText": "取消", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalConfirmButtonText": "禁用高级编辑器", + "xpack.transform.stepDefineForm.advancedEditorSwitchModalTitle": "未应用的更改", + "xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText": "应用更改", + "xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel": "高级查询编辑器", + "xpack.transform.stepDefineForm.advancedSourceEditorHelpText": "高级编辑器允许您编辑转换配置的源查询子句。", + "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalBodyText": "切换回到查询栏,您将会丢失编辑。", + "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalConfirmButtonText": "切换至查询栏", + "xpack.transform.stepDefineForm.advancedSourceEditorSwitchModalTitle": "编辑将会丢失", + "xpack.transform.stepDefineForm.aggExistsErrorMessage": "名称为“{aggName}”的聚合配置已存在。", + "xpack.transform.stepDefineForm.aggregationsLabel": "聚合", + "xpack.transform.stepDefineForm.aggregationsPlaceholder": "添加聚合……", + "xpack.transform.stepDefineForm.groupByExistsErrorMessage": "名称为“{aggName}”的分组依据配置已存在。", + "xpack.transform.stepDefineForm.groupByLabel": "分组依据", + "xpack.transform.stepDefineForm.groupByPlaceholder": "添加分组依据字段……", + "xpack.transform.stepDefineForm.indexPatternLabel": "索引模式", + "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "查询无效:{errorMessage}", + "xpack.transform.stepDefineForm.maxSubAggsLevelsLimitMessage": "您已达到可在表单中添加的最大子聚合级别数。如果想再添加一个级别,请编辑 JSON 配置。", + "xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage": "无法添加配置“{aggName}”,因为与“{aggListName}”有嵌套冲突。", + "xpack.transform.stepDefineForm.nestedConflictErrorMessage": "无法添加配置“{aggName}”,因为与“{aggNameCheck}”有嵌套冲突。", + "xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage": "无法添加配置“{aggName}”,因为与“{groupByListName}”有嵌套冲突。", + "xpack.transform.stepDefineForm.queryPlaceholderKql": "例如,{example}", + "xpack.transform.stepDefineForm.queryPlaceholderLucene": "例如,{example}", + "xpack.transform.stepDefineForm.savedSearchLabel": "已保存搜索", + "xpack.transform.stepDefineSummary.aggregationsLabel": "聚合", + "xpack.transform.stepDefineSummary.groupByLabel": "分组依据", + "xpack.transform.stepDefineSummary.indexPatternLabel": "索引模式", + "xpack.transform.stepDefineSummary.queryCodeBlockLabel": "查询", + "xpack.transform.stepDefineSummary.queryLabel": "查询", + "xpack.transform.stepDefineSummary.savedSearchLabel": "已保存搜索", + "xpack.transform.stepDetailsForm.advancedSettingsAccordionButtonContent": "高级设置", + "xpack.transform.stepDetailsForm.continuousModeAriaLabel": "选择延迟。", + "xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText": "选择可用于标识新文档的日期字段。", + "xpack.transform.stepDetailsForm.continuousModeDateFieldLabel": "日期字段", + "xpack.transform.stepDetailsForm.continuousModeDelayError": "延迟格式无效", + "xpack.transform.stepDetailsForm.continuousModeDelayHelpText": "当前时间和最新输入数据时间之间的时间延迟。", + "xpack.transform.stepDetailsForm.continuousModeDelayLabel": "延迟", + "xpack.transform.stepDetailsForm.continuousModeError": "连续模式不可用于没有日期字段的索引。", + "xpack.transform.stepDetailsForm.destinationIndexHelpText": "已存在具有此名称的索引。请注意,运行此转换将会修改此目标索引。", + "xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel": "选择唯一目标索引名称。", + "xpack.transform.stepDetailsForm.destinationIndexInvalidError": "目标索引名称无效。", + "xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink": "详细了解索引名称限制。", + "xpack.transform.stepDetailsForm.destinationIndexLabel": "目标 IP", + "xpack.transform.stepDetailsForm.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}", + "xpack.transform.stepDetailsForm.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}", + "xpack.transform.stepDetailsForm.errorGettingIndexNames": "获取现有索引名称时发生错误:", + "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", + "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", + "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "提取转换预览时发生错误", + "xpack.transform.stepDetailsForm.frequencyAriaLabel": "选择频率。", + "xpack.transform.stepDetailsForm.frequencyError": "频率格式无效", + "xpack.transform.stepDetailsForm.frequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。", + "xpack.transform.stepDetailsForm.frequencyLabel": "频率", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldHelpText": "选择用于全局时间筛选的主要时间字段。", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldLabel": "时间字段", + "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel": "选择最大页面搜索大小。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeError": "max_page_search_size 必须是介于 10 到 10000 之间的数字。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeHelpText": "定义用于每个检查点的组合聚合的初始页面大小。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeLabel": "最大页面搜索大小", + "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "我不想使用时间筛选", + "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", + "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", + "xpack.transform.stepDetailsForm.transformDescriptionPlaceholderText": "描述(可选)", + "xpack.transform.stepDetailsForm.transformIdExistsError": "已存在具有此 ID 的转换。", + "xpack.transform.stepDetailsForm.transformIdInputAriaLabel": "选择唯一的作业 ID。", + "xpack.transform.stepDetailsForm.transformIdInvalidError": "只能包含小写字母数字字符(a-z 和 0-9)、连字符和下划线,并且必须以字母数字字符开头和结尾。", + "xpack.transform.stepDetailsForm.transformIdLabel": "作业 ID", + "xpack.transform.stepDetailsSummary.advancedSettingsAccordionButtonContent": "高级设置", + "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "连续模式日期字段", + "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "将为此作业创建 Kibana 索引模式。", + "xpack.transform.stepDetailsSummary.destinationIndexLabel": "目标 IP", + "xpack.transform.stepDetailsSummary.frequencyLabel": "频率", + "xpack.transform.stepDetailsSummary.indexPatternTimeFieldLabel": "Kibana 索引模式时间字段", + "xpack.transform.stepDetailsSummary.maxPageSearchSizeLabel": "最大页面搜索大小", + "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "转换描述", + "xpack.transform.stepDetailsSummary.transformIdLabel": "作业 ID", + "xpack.transform.tableActionLabel": "操作", + "xpack.transform.toastText.closeModalButtonText": "关闭", + "xpack.transform.toastText.modalTitle": "错误详细信息", + "xpack.transform.toastText.openModalButtonText": "查看详情", + "xpack.transform.transformForm.sizeNotationPlaceholder": "示例:{example1}、{example2}、{example3}、{example4}", + "xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage": "已成功删除 {count} 个目标索引{count, plural, one {模式} other {模式}}。", + "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "已成功删除 {count} 个目标{count, plural, one {索引} other {索引}}。", + "xpack.transform.transformList.bulkDeleteModalTitle": "删除 {count} 个 {count, plural, one {转换} other {转换}}?", + "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "已成功删除 {count} 个{count, plural, one {转换} other {转换}}。", + "xpack.transform.transformList.bulkStartModalTitle": "启动 {count} 个 {count, plural, one {转换} other {转换}}?", + "xpack.transform.transformList.cloneActionNameText": "克隆", + "xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "一个或多个转换为已完成批量转换,无法重新启动。", + "xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} 为已完成批量转换,无法重新启动。", + "xpack.transform.transformList.createTransformButton": "创建转换", + "xpack.transform.transformList.deleteActionDisabledToolTipContent": "停止数据帧作业,以便将其删除。", + "xpack.transform.transformList.deleteActionNameText": "删除", + "xpack.transform.transformList.deleteBulkActionDisabledToolTipContent": "一个或多个选定数据帧转换必须停止,才能删除。", + "xpack.transform.transformList.deleteModalCancelButton": "取消", + "xpack.transform.transformList.deleteModalDeleteButton": "删除", + "xpack.transform.transformList.deleteModalTitle": "删除 {transformId}?", + "xpack.transform.transformList.deleteTransformErrorMessage": "删除转换 {transformId} 时发生错误", + "xpack.transform.transformList.deleteTransformGenericErrorMessage": "调用用于删除转换的 API 终端节点时发生错误。", + "xpack.transform.transformList.deleteTransformSuccessMessage": "删除转换 {transformId} 的请求已确认。", + "xpack.transform.transformList.editActionNameText": "编辑", + "xpack.transform.transformList.editFlyoutCalloutDocs": "查看文档", + "xpack.transform.transformList.editFlyoutCalloutText": "此表单允许您更新转换。可以更新的属性列表是创建转换时可以定义的列表子集。", + "xpack.transform.transformList.editFlyoutCancelButtonText": "取消", + "xpack.transform.transformList.editFlyoutFormAdvancedSettingsButtonContent": "高级设置", + "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "描述", + "xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "目标配置", + "xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "目标索引", + "xpack.transform.transformList.editFlyoutFormDestinationPipelineLabel": "管道", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "要启用节流,请设置每秒要输入的文档限值。", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "每秒文档数", + "xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。", + "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "频率", + "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "频率值无效。", + "xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "定义用于每个检查点的组合聚合的初始页面大小。", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大页面搜索大小", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "值必须是大于零的整数。", + "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "值必须是介于 10 到 10000 之间的整数。", + "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必填字段。", + "xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage": "值需要为字符串类型。", + "xpack.transform.transformList.editFlyoutTitle": "编辑 {transformId}", + "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", + "xpack.transform.transformList.editTransformGenericErrorMessage": "调用用于更新转换的 API 终端时发生错误。", + "xpack.transform.transformList.editTransformSuccessMessage": "转换 {transformId} 已更新。", + "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "检查用户是否可以删除目标索引时发生错误", + "xpack.transform.transformList.refreshButtonLabel": "刷新", + "xpack.transform.transformList.rowCollapse": "隐藏 {transformId} 的详情", + "xpack.transform.transformList.rowExpand": "显示 {transformId} 的详情", + "xpack.transform.transformList.showDetailsColumn.screenReaderDescription": "此列包含可单击控件,用于显示每个转换的更多详情", + "xpack.transform.transformList.startActionNameText": "启动", + "xpack.transform.transformList.startedTransformBulkToolTip": "一个或多个选定数据帧转换已启动。", + "xpack.transform.transformList.startedTransformToolTip": "{transformId} 已启动。", + "xpack.transform.transformList.startModalBody": "转换将增加集群的搜索和索引负载。如果超负荷,请停止转换。", + "xpack.transform.transformList.startModalCancelButton": "取消", + "xpack.transform.transformList.startModalStartButton": "启动", + "xpack.transform.transformList.startModalTitle": "启动 {transformId}?", + "xpack.transform.transformList.startTransformErrorMessage": "启动转换 {transformId} 时发生错误", + "xpack.transform.transformList.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", + "xpack.transform.transformList.stopActionNameText": "停止", + "xpack.transform.transformList.stoppedTransformBulkToolTip": "一个或多个选定数据帧转换已停止。", + "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} 已停止。", + "xpack.transform.transformList.stopTransformErrorMessage": "停止数据帧转换 {transformId} 时发生错误", + "xpack.transform.transformList.stopTransformResponseSchemaErrorMessage": "调用停止转换请求时发生错误。", + "xpack.transform.transformList.stopTransformSuccessMessage": "停止数据帧转换 {transformId} 的请求已确认。", + "xpack.transform.transformList.transformDescription": "使用转换将现有 Elasticsearch 索引切换到摘要式或以实体为中心的索引。", + "xpack.transform.transformList.transformDetails.messagesPane.errorMessage": "无法加载消息", + "xpack.transform.transformList.transformDetails.messagesPane.messageLabel": "消息", + "xpack.transform.transformList.transformDetails.messagesPane.nodeLabel": "节点", + "xpack.transform.transformList.transformDetails.messagesPane.timeLabel": "时间", + "xpack.transform.transformList.transformDetails.tabs.transformDetailsLabel": "详情", + "xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel": "消息", + "xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel": "预览", + "xpack.transform.transformList.transformDetails.tabs.transformStatsLabel": "统计", + "xpack.transform.transformList.transformDocsLinkText": "转换文档", + "xpack.transform.transformList.transformTitle": "数据帧作业", + "xpack.transform.transformsDescription": "使用转换将现有 Elasticsearch 索引透视成摘要式或以实体为中心的索引。", + "xpack.transform.transformsTitle": "转换", + "xpack.transform.transformsWizard.cloneTransformTitle": "克隆转换", + "xpack.transform.transformsWizard.createTransformTitle": "创建转换", + "xpack.transform.transformsWizard.stepConfigurationTitle": "配置", + "xpack.transform.transformsWizard.stepCreateTitle": "创建", + "xpack.transform.transformsWizard.stepDetailsTitle": "作业详情", + "xpack.transform.transformsWizard.transformDocsLinkText": "转换文档", + "xpack.transform.wizard.nextStepButton": "下一个", + "xpack.transform.wizard.previousStepButton": "上一页", "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "公测版", "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "此操作位于公测版中,可能会有所更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。请通过报告任何错误或提供其他反馈来帮助我们。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "更改", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index ef81065608ad4..3e5e95996c80f 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -25,6 +25,7 @@ Table of Contents - [GROUPED BY expression component](#grouped-by-expression-component) - [FOR THE LAST expression component](#for-the-last-expression-component) - [THRESHOLD expression component](#threshold-expression-component) + - [Alert Conditions Components](#alert-conditions-components) - [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin) - [Build and register Action Types](#build-and-register-action-types) - [Built-in Action Types](#built-in-action-types) @@ -634,6 +635,155 @@ interface ThresholdExpressionProps { |customComparators|(Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`.| |popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.| +## Alert Conditions Components +To aid in creating a uniform UX across Alert Types, we provide two components for specifying the conditions for detection of a certain alert under within any specific Action Groups: +1. `AlertConditions`: A component that generates a container which renders custom component for each Action Group which has had its _conditions_ specified. +2. `AlertConditionsGroup`: A component that provides a unified container for the Action Group with its name and a button for resetting its condition. + +These can be used by any Alert Type to easily create the UI for adding action groups along with an Alert Type specific component. + +For Example: +Given an Alert Type which requires different thresholds for each detected Action Group (for example), you might have a `ThresholdSpecifier` component for specifying the threshold for a specific Action Group. + +``` +const ThresholdSpecifier = ( + { + actionGroup, + setThreshold + } : { + actionGroup?: ActionGroupWithCondition<number>; + setThreshold: (actionGroup: ActionGroupWithCondition<number>) => void; +}) => { + if (!actionGroup) { + // render empty if no condition action group is specified + return <Fragment />; + } + + return ( + <EuiFieldNumber + value={actionGroup.conditions} + onChange={(e) => { + const conditions = parseInt(e.target.value, 10); + if (e.target.value && !isNaN(conditions)) { + setThreshold({ + ...actionGroup, + conditions, + }); + } + }} + /> + ); +}; + +``` + +This component takes two props, one which is required (`actionGroup`) and one which is alert type specific (`setThreshold`). +The `actionGroup` will be populated by the `AlertConditions` component, but `setThreshold` will have to be provided by the AlertType itself. + +To understand how this is used, lets take a closer look at `actionGroup`: + +``` +type ActionGroupWithCondition<T> = ActionGroup & + ( + | // allow isRequired=false with or without conditions + { + conditions?: T; + isRequired?: false; + } + // but if isRequired=true then conditions must be specified + | { + conditions: T; + isRequired: true; + } + ) +``` + +The `condition` field is Alert Type specific, and holds whichever type an Alert Type needs for specifying the condition under which a certain detection falls under that specific Action Group. +In our example, this is a `number` as that's all we need to speciufy the threshold which dictates whether an alert falls into one actio ngroup rather than another. + +The `isRequired` field specifies whether this specific action group is _required_, that is, you can't reset its condition and _have_ to specify a some condition for it. + +Using this `ThresholdSpecifier` component, we can now use `AlertConditionsGroup` & `AlertConditions` to enable the user to specify these thresholds for each action group in the alert type. + +Like so: +``` +interface ThresholdAlertTypeParams { + thresholds?: { + alert?: number; + warning?: number; + error?: number; + }; +} + +const DEFAULT_THRESHOLDS: ThresholdAlertTypeParams['threshold] = { + alert: 50, + warning: 80, + error: 90, +}; +``` + +``` +<AlertConditions + headline={'Set different thresholds for each level'} + actionGroups={[ + { + id: 'alert', + name: 'Alert', + condition: DEFAULT_THRESHOLD + }, + { + id: 'warning', + name: 'Warning', + }, + { + id: 'error', + name: 'Error', + }, + ]} + onInitializeConditionsFor={(actionGroup) => { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} +> + <AlertConditionsGroup + onResetConditionsFor={(actionGroup) => { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + <TShirtSelector + setTShirtThreshold={(actionGroup) => { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + </AlertConditionsGroup> +</AlertConditions> +``` + +### The AlertConditions component + +This component will render the `Conditions` header & headline, along with the selectors for adding every Action Group you specity. +Additionally it will clone its `children` for _each_ action group which has a `condition` specified for it, passing in the appropriate `actionGroup` prop for each one. + +|Property|Description| +|---|---| +|headline|The headline title displayed above the fields | +|actionGroups|A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow| +|onInitializeConditionsFor|A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field| + +### The AlertConditionsGroup component + +This component renders a standard EuiTitle foe each action group, wrapping the Alert Type specific component, in addition to a "reset" button which allows the user to reset the condition for that action group. The definition of what a _reset_ actually means is Alert Type specific, and up to the implementor to decide. In some case it might mean removing the condition, in others it might mean to reset it to some default value on the server side. In either case, it should _delete_ the `condition` field from the appropriate `actionGroup` as per the above example. + +|Property|Description| +|---|---| +|onResetConditionsFor|A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup| + + ## Embed the Create Alert flyout within any Kibana plugin Follow the instructions bellow to embed the Create Alert flyout within any Kibana plugin: diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index bf54ab3f91045..b8514a06dc253 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -15,6 +15,7 @@ import { ActionTypeModel } from '../../../types'; import { getServiceNowActionType } from './servicenow'; import { getJiraActionType } from './jira'; import { getResilientActionType } from './resilient'; +import { getTeamsActionType } from './teams'; export function registerBuiltInActionTypes({ actionTypeRegistry, @@ -30,4 +31,5 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getServiceNowActionType()); actionTypeRegistry.register(getJiraActionType()); actionTypeRegistry.register(getResilientActionType()); + actionTypeRegistry.register(getTeamsActionType()); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/index.ts new file mode 100644 index 0000000000000..da407f786292a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getActionType as getTeamsActionType } from './teams'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.svg new file mode 100644 index 0000000000000..ab07be8f1ef0a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.svg @@ -0,0 +1,131 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve"> <image id="image0" width="256" height="256" x="0" y="0" + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABGdBTUEAALGPC/xhBQAAACBjSFJN +AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAA +CXBIWXMAAA7DAAAOwwHHb6hkAAAbM0lEQVR42u3deZhU9Z0u8Pf9nVNVve/sYHSiaFxiRJYnmXvj +cp2ZaK4BTUAN6pCJA7mONzdxiUq3Wko3Ro3GubnDjEsCE0ENHQV0opkb3DNBlugljlsMM0YEFBro +ppvu6q4653v/AJcg3V3dXad+p+p8P8/Do1hVp97fsc/bZz+AUkoppZRSSimllFJKKaWUUkoppZRS +SimlChdtB1DhkbxHyjLtbZPcNCp9HvgDAEbQaQSdmRg63ZqGrckF7LadVeWGFkBEJe/qqMuk0qeL +j9MIHA/gWAEmQmTgnwlSCLwL4E0BXqPBc25J7NnkldV7bI9JDZ0WQIQ03d5xDHr7LhHiXIAnD7qw +Z4sUQDZT8DgS8Qeav1f9lu2xquxoARS55FIp8ba3XSo+5wnk8/n4ToLrAC51J9Y9kPwGU7bngeqf +FkCRuuMOKe/oa/uWCK6CYJyVEMQOEndWxxv+6ZpruN/2PFGfpAVQZESENy5u+1vx0SJAg+08AECg +jQaNtyxsuI+k2M6jPqIFUESSLe1TMn5miUBm2M5yOATXu8a9PNlY85LtLOoALYAikBQx6ZbdCwEk +IeLYzjMg0iN5s7uwriVJ+rbjRJ0WQIFraeka0yOp5SJylu0sQ0FybSlLLm5srHjfdpYo0wIoYDd8 +f+/JkvaeEMh421mGg+B2xpxzFl1Xu9l2lqgytgOo4blh8d7T/Iz3XKEu/AAgkPF+xnvuhsV7T7Od +Jaq0AArQjc27zhUv80uIVNvOMmIi1eJlfnlj865zbUeJIt0EKDA3LN57mniZXwpQYjtLLhFI0XG/ +tGhh7XO2s0SJFkABueH7e0/2M95zRfGb/3DIDuM6p+k+gfzRAigQLS1dY3r81EuFvM2fDYLbS03J +FD06kB+6D6AAJEVMj6SWF/vCDxzYMdgjqeVJEf3ZzAOdyQUgs3hPY6Ed5x8JETkrs3hPo+0cUaCb +ACGXbGmfkpbMhtCf4ZdrpBejO11PGw6WrgGEmIgw42eWRG7hPzB4J+Nnlkiu7lmgDit6P1gFxI9f +MV8E37Kdw6KJz/66e/vzT9/xW9tBipW2a0jdcYeUd6Ta3g7LJb22EGirLmk4Uu8nEAzdBAipjr62 +b0V94QcAARo6+tqivBYUKF0DCKHkUilJb2v7D2t38gkbYkdsQsOfFcLtxWavFMdbs3GK+PJfAR4n +kOMATCJZCTlwl2UQnSLSCWArwTcAeYOGLzgzp73UOodePvNqAYTQDS275vs+7rGdI0yMwYJFjaPu +tZ3jcObP3xTb1SXnQPyLAf6FYHhnahLsAORXoFk+qoJP3Hvv1HTQ2bUAQqhpUdtv8nUDz0JBcF3z +DQ1fsJ3j42Z/d2tpeueO71BwlYjU53S85G4h7oyNHnd36w8n9QQ1Bi2AkGm6veMY6e37ve0cYcRE +fHIYbjk+e6U43upN80T8mwWYEOiYgW2kucmZNXVZEJsHuhMwbHr7LrEdIbRCMG9mzd14dnr1ht/5 +4t8f9MIPAAJM8MW/P716w+9mzd14dq6nrwUQMgce2qEOx+a8ERGe9/X1LfD9JyA4Pv8BcDx8/4nz +vr6+JZcnR+kmQIgk7+qoS3en23L2xJ5iQ0qsLNaQ78eQzb781YpMe9dyEcy0PQsOzAascWsqLm5d +ckLXSKelawAhkkmlT9eFfwAizKTSp+fzKy+c9/KR6fb9vwnLwn9gNmBmun3/by6c9/KRI52WFkCI +iA+9N94g8jmPLpz38pG9fX3rIXKS7XF/ckbISb19fetHWgJaACFy8Cm9agD5mkezL3+1ItWXfkwE +o22PuT8iGJ3qSz82+/JXK4Y7DS2AcDnWdoACEPg8EhFm2ruWh/I3/yfDnnRg/8TwNh21AEIieY+U +CTDRdo6wE2Bi8h4pC/I7zp+7oTlM2/yDzhPBzPPnbmgezme1AEIi0942SXcAZkGEmfa2SUFNftbc +jWeLYKHtYQ6VCBYO5zwBLYCQoM8q2xkKRVDzavZKcSD+D2yPb9jE/8HslUO7eYwWQEhI5uCVYmpQ +Qc0rb/WmeVZO8skVwfHe6k3zhvIRLYCwcGTYe3IjJ4B5Nfu7W0tF/JttD22kRPybZ393a2m279cC +UApAeueO7+Tj3P6gCTAhvXPHd7J9vxZASBhBp+0MhSLX82r+/E0xCq6yPa5coeCq+fM3xbJ5rxZA +SGgBZC/X82pXl5yT6+v5bRKR+l1dck4279UCCIlMTAsgW7meVwKxfplxzol/cTZv0wIICbemYStI +sZ0j9Ehxaxq25mpys1eKQ0ERPnWJf5HNIUEtgJBILmA3gXdt5wg7Au8mF7A7V9Pz1mycMtx7+IWZ +QKq9NRunDPY+LYBwedN2gAKQ03l04O69xSmbsWkBhIgAr9nOEHY5n0fkZ2yPKTBZjM21nVF9pLPz +vXXw5du2cwAAjYGhA9dNwHVLYJxw/KjQ4LlcTk9EivYKzGzGFo7/qwoAkMqk1paYEgHsXxQkvg8P +Pjwvjd7eLsRipYgnKmCMxcdJklKGeE4LAEBgFxaFwKBj002AELn3zqltNPx32zkOJ53uQff+NniZ +PospZPPChVW7czlFkkV7DUY2Y9MCCBuRJ2xH6D+aoLtnj7USoODx3A+qiC/CymJsWgAhk4b8s+0M +AxKgp2cvfD+vj7A7IBF/wPbwi40WQMgsuf2k1wlstJ1jICKCvt4R35F6SAiuC+SpQCziMzCzGJsW +QAgZmp/azjCYdLoHvpfJ4zdyaRBTPfiU3qKUzdi0AEJob1nZ/SDet51jMJlMnp7WTexwJ9YFtfqf +s9OKQ2jQsfV7GHDWBU99Ou3hawTOBuQoAcdBJKtLDNXI7HplC3aNaAoCSB9E+uB77RBvD0R6P3qZ +/PD4fnnFGFRVjUeiZOj32MhkehFPBH8fExJ3Jr/BQNqG5JsixXk2IME3BnvPJwpg9uxnxnb70pzx +/HkQOB9dnaLXqRQOAkyATMAxlUBsIvzMbviZrRBJAyLIpFPIpFNI9bRj9643UVU9EQ2jj0U8nv0N +d30JfkcggbbqeMM/BfYFIq8HPghrZGgFMPOrT8/o9r1VAMaJLu9FhDBuA+hUw+t7C+J/cgfevo53 +sb9rJyZMmoqy8uwujRffDz65QeM113B/cNPnC+IX5w87DV8Y7D0f7gOY+dWnZ2QozwIYZzu4CgYZ +g5v4DGgOv9rueX3Y+sf16N6f03Nthp8XXH/Lwob7gvwOZ+a0lwh22B5rrhHscGZOe2mw9xngwGp/ +Bv4qiJTYDq6CRjjxY0AefneOiIdtWzehry9nV9wOMyY917iXM+B7JLTOoSfEWruDDYL8qnUOB91G +MwDQ7Usz9Dd/ZJAxGLf/08Q9rw9tO+1emUzy5mRjzaC/wXLyXWDxnWBEszybt5lZFzz1adKfZzuv +yi/j1oNM9Pv6vo530ZvK78k+HyC51l1Y15Kv7xtVwSdIhmO7JwdI7h5VwaxOKTdpD18TgcVLvJQd +BJ26Ad+xb992G6m2l7Lk4iQZ/B7Gg+69d2paiDvzPtiACHHnvfdOTWfzXnPgOL+KIuPUDPj6/q48 +n4tEdjDmnNPYWJH3k6Bio8fdTWBbvr831whsi40ed3e27zeAHGU7tLKDjA/4et7O9ANAIGWMM3PR +dbWbbcyL1h9O6iHNTTa+O5dIc1PrDyf1ZPt+I6Du/IuqQQugN8sJjTAGkDLEnEULa3N9s48hcWZN +XQYW8G3ZiNecWVOXDeUjRk/vjbJBbjyUj7PByA467pduaRqV+2v9h6h1Dj3QXG07x7DRXJ3Nob+P +04uBlDUEtxvXOc32b/6PW71i2pMkFtvOMVQkFq9eMe3JoX5OC0BZQXJtqSmZYmubfyCPrpjeRGKN +7RzZIrHm0RXTm4bzWS0AlV+kR2NudBvr/8rG3v7sIlLcmoqLQb5iO0sWYV9xayouHu4Zk1oAKm8I +ro/Rnd7cWL8on8f5h6N1yQldJfHYV0jstJ2lPyR2lsRjX2ldcsKwz9jSAlCBI9BmDBYsaqr/fL5O +782Fh5ed8nYiHp8RyjUB8pVEPD7j4WWnvD2SyWgBqOAQO2hwdXVJw5GLGkfdG/SFPUF4eNkpb8dq +yr8Qpn0CJNbEasq/MNKFH9AHg6ggkC8aylJnfMNPg7qTTz61LjmhS0TOO3/uhmYRLLSZhcTiAzsp +c1OmWgBq5EgBZDMFjyMRfyCQu/daHyIFQOOsuRt/DfF/AMHx+Q2A10Bz9aoV057kg7mbrBZAxJWU +1bVmMj3HZNKpSRDUDf5YMgoh20nzBx94nQ5/VYb4c7l+Yk9YrV4x7cnZK+X/eqs3zRPxbxZgQpDf +R2AbaW5yZk1dNtSTfLKa/pe/urbgtstU7vzikbM+XOBPPffxsk9V7z/OQU99X2+mLu311Hz66NN2 +i+d0unF/nwenoz2d+eMDPzg5sFt0FZLZ391amt654zsUXCUi2d1HLUskdwtxZ2z0uLuHcm7/kL9H +CyDaPl4Aanjmz98U29Ul5wjkEgrOEkj1cKZDsEOItQQfGFXBJ7K9pHckdBNAqRE6uKCuAbBm9kpx +0qs3nUrx/wvIzxx8RPckkpUfPquP6Dz40I6tJN+EyOtC82t31tTfBrGaPxDrBXDrzVNw3ORhFWbB +e/OtDlx3Y8EcFldZOLgAbzj4Z2hyuHMvW9YLwHWIWCyapyPE3GiOW4WH/gQqFWFaAEpFmBaAUhGm +BaBUhGkBKBVhWgBKRZj1w4DKrr88Z/GAZ4Ied8K5A36+smqs7SEMie/7yKQ99KbT2N/VjY593chk +MrZj5QyBLoDbQWwB+QsivubRFSe/29/7dQ1ARYoxBvFEDJUVZRg7tgGTJx+B8eMaEHOL4+FYAlQI +ZLKInC2+/3/ET7193tc33Hfh37w0/rDzw3ZgpWwigNraKhx99BGorCyzHSfnBHBE5LJUKv3GVy/a +8InVOS0ApQAYQxwxaSzq64v0tHRBpU+sPn/u+v/5J+O2nUupMBk7pr4o1wQAQESMCO/++JqAFoBS +h5g4YUzR7BM4lIgYj7Lig30CWgBKHcIYYtSoWtsxgiOo7O3JJIEQHAbcum0/nACvips0oQylpcMb +puf52PKfw77l+qDeeVdvrBNWNbVV2LmrvagOEf4p+Zvz526+xXoB/O9/fCPQ6d/efCqOP65mWJ/t +6srgyus2WpgryjYCqK4qw+49+2xHCYQADtA3UzcBlOpHeUVx7gz8gIh/jhaAUv1IxGK2IwSMn9YC +UKofbqw4jwR8RMZrASjVD2OKfPEQVBb5CJVSA9ECUCrCtACUijAtAKUiTAtAqQjTAlAqwrQAlIow +LQClIkwLQKkI0wJQKsK0AJSKMC0ApSJMC0CpCNMCUCrCtACUijAtAKUiTAtAqQjTAlAqwrQAlIow +LQClIkwLQKkI0wJQKsK0AJSKMC0ApSJMC0CpCNMCUCrCtACUijAtAKUiTAtAqQjTAlAqwrQAlIow +LQClIkwLQKkI0wJQKsK0AJSKMC0ApSJMC0CpCNMCUCrCtACUijAtAKUiTAtAqQjTAlAqwrQAlIow +LQClIkwLQKkI0wJQKsK0AJSKMC0ApSJMC0CpCNMCUCrCtACUijAtAKUiTAtAqQjTAlAqwrQAlIow +LQClIkwLQKkI0wJQKsJc2wGC1tfnI5XyhvXZVO/wPqdUISDpF30BNN3ysu0ISoWToFM3AZSKKAH2 +aQEoFVEEOrQAlIooXQNQKsJIvKMFoFRECfCqFoBSEeWI+XctAKWiio6uASgVScS+kyZ/bosWgFKR +xF8lk/S1AJSKIAM+eeCfSqnIiZc4WgBKRRNffvgnU7YDgAGZth1H2SK2AygLaPjjD/7dELLDdiBl +h+/12I6g8o3cX4aaBz74qwH4n7YzKTs8r9t2BJV/K1asOGbfB38xAjxpO5Gyo6/3fdsRQk2k+DaR +aNwlH/+7iTn4OQm99U3kCNKpbbZDhJr4xVUABH6+avmUzR//b2b1z/7bFhGzzHY4lV+p7nfgeftt +xwg1v4jWAEimXce9/tD/bgCgzLAJgO4MjAjfS6Gn61XbMULP93zbEXKGkHtal5/6h0P/uwGA1tYz +3nNhzgOZsh1UBUvER2f7i/D9XttRQq9o1gCIvU4idsvhXvrwRKA1j5y53hWeDl0TKFq+l8K+Pc8j +k96b5SdoO7JVvl8cu8YIc3nr0im7Dvfan5wJuOaRM9eXGWcKYH6sOwaLiSDV/Ud07H76TxZ+0hnw +U8Y4g024qPWli2ARIB9c9eC0h/t7+RO3BW9tPeM9AJfNuuCpW9MevkbgbECOEnAcRGK2x6OyIfC9 +HnheN/p630c6te2wO/wGW8CNKfq7xg8onS70k2S5tSYe+7sB32E7Ytidflbjj1I97VfYzhGEREkV +EonKfl8vLavDp4768wGnUVk11vYwArNt+y60t3fajjEsJFIQnLnqoRnrBnpftCs+C8aJbR75VMIp +5pYO+Ho8Xm47olXpdMZ2hGEh6RvhxY88NG3dYO/VqwEH0dPjrbedIQixeBmM03//JxKVcJxob/H1 +9RXmJoAAVz7y0LRHsnmvFsAg1j2/6BWQRXXMjDRIJKr6fd0YB6VltbZjWuV7fqGuAdy++sHpf5/t +m7UAsuCYWFGdM1taVj/ADkCiomJM5HcAdqcKr/MJXr/6oRnXDuUzWgBZcGJlr9jOkAukQVn5KLhu +/LCvG+Ogqmoc3FiJ7ajW9XQXzjlxBDwa881VD03//lA/qwWQBePEXrSdYaRi8TKUV4zud+FPJCpR +VT1hSAs/WbwHkXoKZQ2A2GfA81atmPaT4Xw82ut5WepNeb8AcKvtHENBOjDGgRsrQcwtPWSHH2GM +A2NcxOJliMfLh7XDr5g3E3p6+mxHGBTJda6TmNv6wMnDvqdH8VZ4jl106c/eEZFJtnPk2+ixJ/T7 +WjxRgUSiwnbEnEv19mHLlndtx+gXSR/krScfMzWZTHJEeyqLt8JzzcSehtf317ZjhEnMLc59BZ2d +4b1TEsmNEPlfqx6cvm5VDqanBZAlY2SN50EL4CA3VjLgeQSFrHt/6nUAn7Gd4+NIvCvk9Y8un7aC +ZM4uU9SdgFl6Y3PbvwDUO2jgwM6/gU4hLmQE2o6+78QTjWNmkrC/85d8hzDfc8eMn7x6xfTluVz4 +AV0DyNpvf7sgPfnE1mfF975sO4tVBEpLa4v4SkE+kSR9AI8BeOz8r286Q+BfAcF/F0h8pFPPOgXx +DIkfOTOnPdY6h4FdlqgFMASO4y7NRKkADjnMRxKlpbVw3LwtB3lnDJd//O+PPjj1GQDPnHfp+npk +zEUQuYiQGQLktAEJeAL8hjCPI+avWfXTGb8HAKwIdrx6FGBozIWXrHwf8BtsB8kHx02gvuFoAEAs +Vop4oqKIf/MDIN9ubqz/s8FWs+fNe7lmXzpzlvjylyA+B8FkgVQP7buwF8LNJDaTskEc/Ouqn87Y +ne8h6xrA0PiucdZkfP+btoMEjcZBPF6ORKICbqy0uBf8D8ZM/iSbbexly05pB/Dzg38AABdd9sqY +3u6eYwWYJGC5IcogKBNKAoJOkHsg2O0Ys8eNeW8/vHT6VtvjBbQAhqy8atRdmUzfN0SKfwdqVfV4 +xIvwOP/hEPRdF0uH+/mH7j/pfQAF96CFov8hzrV7fvTF10Cz1naOwBEoKRnaWm0hE8qTyWvrw3v2 +T0C0AIaD7u22IwStrKyuqHf2HcoY5zbbGayM23aAQvTjf/jiUzTO/7OdIyjGcVFZOc52jLwh8fyi +hXUv2M5hgxbAMBGxRbYzBDIuErW1nyras/wOS1iU/y+zoYcBR+Cyy5/+N9/3v2A7R64Y46C27sjI +7PgDAJAvtjQ1fN52DFt0DWAEHCauBnJ7aqYtpWU1GDX6uGgt/AAc8kbbGWzSNYARuuzvnnnU97zz +bOcYKuO4cJ0E4okKlJbVwnUTtiPlH7mqpanhfNsxbIrQhl4wJh5x4hXpNM+EDPFMMGUVgZTrOFfa +zmGbbgKMUPLaUdsN5DrbOdSQ3Za8vvZt2yFs0wLIgVsaG+4h8G+2c6gskW/XlTdE8rj/obQAcoCk +uI6ZTzD8N5KLOII+4Pz1lVeyx3aWMNACyJHkwvrXhGyxnUMNjMTtLU21z9vOERZaADl08uS6FpDP +2M6h+kG8NHFUfaQP+x1KCyCH5syhV8aSiwi8ZzuLOgTREzOcu2ABC/OBfwHRAsixxsaK9wFcBAZ3 +Gyc1HM43kwsb3rCdImy0AALQfMOoZwHoqmZIGLK5panuIds5wkjPBAxQY3PbUojMs50jygg+uqip +/mu5vptusdA1gADF/rz+bwk8YTtHVBF42R1df4ku/P3TAghQ8gxm3NENs0FusJ0lcsg3Sk3p2ckF +DO9jfkJACyBgyQXsjpXEv0zwTdtZooLAW2UsPfPgDlk1AC2APEheXdXmliS+COJ3trMUPeI/Ssgz +GhvLd9iOUgi0APIkeU3lzgondjpI+4+bKlIktsRc54ympoZttrMUCj0KkGfJf5CKzN7da0TkTNtZ +igr5YrlJfGXhwspdtqMUEi0AC5JLpSSzre0nIrjIdpaiQD5SX1Z/iV7gM3RaABY1Nu+6GuD3IVL8 +j90JiCHuchobrjn4QE81RFoAljUtajtLiIchUm87S0EhOwH5Hy1NowJ+fGZx0wIIgaZF7UcBmZ8J +ZJrtLAWBeCnmxC9IXl/9B9tRCp0WQEisXCnO797afR183JjP59AXGoJ/7x5b/73kHL35Si5oAYRM +smXPZ9Pi/TMEn7OdJVSI3zvk5bc0NjxlO0ox0QIIoXvukdg7u3ZfC8h1EJTbzmMTgZTQ3Dqmqu62 +b3+bvbbzFBstgBBL3rZrfDrNFgouFUj0Ttoi/zXmxK7Qbf3gaAEUgGRL+5SMn75LgNNsZ8kHAi8A +uPHgfRVUgLQACkjTol2ng7hWBF+ynSUIBNcBuLH5hoa1trNEhRZAAUq27PlsxvevAXChQAr66U4E +M0KscYh/1B18+acFUMCSt+2emMnIJSK4FCLH2c4zFCS3gryvFCX365V79mgBFIlkc9v0DHEpfLlA +gAbbeQ6HwHtCPObQPHriMXVr58zRG6fapgVQZJIixrt19zQRfgm+/JUQ061da3DgVlyvG+BfYLj6 +luvrXtTbc4WLFkCRu/XW9toeL3O6LzgVkFMATBFgbCBfRnYC2GCAdWLMOrfEeTF5ZfUe2/NA9U8L +IIJaWvaP60Xqs76PIwB/EsiJAkwEMIFAuUBKKEwIJXHwn70UdAqxD8A+CjoBvA9wC8AtdMwfHMff +ctP36rbpb3illFJKKaWUUkoppZRSSimllFJKKTv+PycghAJRYdeEAAAAJXRFWHRkYXRlOmNyZWF0 +ZQAyMDIwLTExLTEyVDE5OjU3OjQ1KzAzOjAw88nh2gAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMC0x +MS0xMlQxOTo1Nzo0NSswMzowMIKUWWYAAAAASUVORK5CYII=" /> +</svg> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx new file mode 100644 index 0000000000000..5343e703628f7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { TeamsActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.teams'; +let actionTypeModel: ActionTypeModel; + +beforeAll(async () => { + const actionTypeRegistry = new TypeRegistry<ActionTypeModel>(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('teams connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + webhookUrl: 'https:\\test', + }, + id: 'test', + actionTypeId: '.teams', + name: 'team', + config: {}, + } as TeamsActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid - empty webhook url', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.teams', + name: 'team', + config: {}, + } as TeamsActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: ['Webhook URL is required.'], + }, + }); + }); + + test('connector validation fails when connector config is not valid - invalid webhook url', () => { + const actionConnector = { + secrets: { + webhookUrl: 'h', + }, + id: 'test', + actionTypeId: '.teams', + name: 'team', + config: {}, + } as TeamsActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: ['Webhook URL is invalid.'], + }, + }); + }); + + test('connector validation fails when connector config is not valid - invalid webhook url protocol', () => { + const actionConnector = { + secrets: { + webhookUrl: 'http://insecure', + }, + id: 'test', + actionTypeId: '.teams', + name: 'team', + config: {}, + } as TeamsActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: ['Webhook URL must start with https://.'], + }, + }); + }); +}); + +describe('teams action params validation', () => { + test('if action params validation succeeds when action params is valid', () => { + const actionParams = { + message: 'message {test}', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { message: [] }, + }); + }); + + test('params validation fails when message is not valid', () => { + const actionParams = { + message: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + message: ['Message is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx new file mode 100644 index 0000000000000..bcfc21d3bfd5d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import teamsSvg from './teams.svg'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { TeamsActionParams, TeamsSecrets, TeamsActionConnector } from '../types'; +import { isValidUrl } from '../../../lib/value_validators'; + +export function getActionType(): ActionTypeModel<unknown, TeamsSecrets, TeamsActionParams> { + return { + id: '.teams', + iconClass: teamsSvg, + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText', + { + defaultMessage: 'Send a message to a Microsoft Teams channel.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle', + { + defaultMessage: 'Send a message to a Microsoft Teams channel.', + } + ), + validateConnector: (action: TeamsActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + webhookUrl: new Array<string>(), + }; + validationResult.errors = errors; + if (!action.secrets.webhookUrl) { + errors.webhookUrl.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText', + { + defaultMessage: 'Webhook URL is required.', + } + ) + ); + } else if (action.secrets.webhookUrl) { + if (!isValidUrl(action.secrets.webhookUrl)) { + errors.webhookUrl.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText', + { + defaultMessage: 'Webhook URL is invalid.', + } + ) + ); + } else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) { + errors.webhookUrl.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText', + { + defaultMessage: 'Webhook URL must start with https://.', + } + ) + ); + } + } + return validationResult; + }, + validateParams: (actionParams: TeamsActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + message: new Array<string>(), + }; + validationResult.errors = errors; + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./teams_connectors')), + actionParamsFields: lazy(() => import('./teams_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx new file mode 100644 index 0000000000000..eaa7159db6a3d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from '@testing-library/react'; +import { TeamsActionConnector } from '../types'; +import TeamsActionFields from './teams_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('TeamsActionFields renders', () => { + test('all connector fields are rendered', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'https:\\test', + }, + id: 'test', + actionTypeId: '.teams', + name: 'teams', + config: {}, + } as TeamsActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + <TeamsActionFields + action={actionConnector} + errors={{ index: [], webhookUrl: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').first().prop('value')).toBe( + 'https:\\test' + ); + }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.teams', + config: {}, + secrets: {}, + } as TeamsActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + <TeamsActionFields + action={actionConnector} + errors={{ index: [], webhookUrl: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.teams', + name: 'teams', + config: {}, + } as TeamsActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + <TeamsActionFields + action={actionConnector} + errors={{ index: [], webhookUrl: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx new file mode 100644 index 0000000000000..41dfc1325e8ed --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { TeamsActionConnector } from '../types'; + +const TeamsActionFields: React.FunctionComponent<ActionConnectorFieldsProps< + TeamsActionConnector +>> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => { + const { webhookUrl } = action.secrets; + + return ( + <Fragment> + <EuiFormRow + id="webhookUrl" + fullWidth + helpText={ + <EuiLink + href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`} + target="_blank" + > + <FormattedMessage + id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel" + defaultMessage="Create a Microsoft Teams Webhook URL" + /> + </EuiLink> + } + error={errors.webhookUrl} + isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel', + { + defaultMessage: 'Webhook URL', + } + )} + > + <Fragment> + {getEncryptedFieldNotifyLabel(!action.id)} + <EuiFieldText + fullWidth + isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} + name="webhookUrl" + readOnly={readOnly} + value={webhookUrl || ''} + data-test-subj="teamsWebhookUrlInput" + onChange={(e) => { + editActionSecrets('webhookUrl', e.target.value); + }} + onBlur={() => { + if (!webhookUrl) { + editActionSecrets('webhookUrl', ''); + } + }} + /> + </Fragment> + </EuiFormRow> + </Fragment> + ); +}; + +function getEncryptedFieldNotifyLabel(isCreate: boolean) { + if (isCreate) { + return ( + <Fragment> + <EuiSpacer size="s" /> + <EuiText size="s" data-test-subj="rememberValuesMessage"> + <FormattedMessage + id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.rememberValueLabel" + defaultMessage="Remember this value. You must reenter it each time you edit the connector." + /> + </EuiText> + <EuiSpacer size="s" /> + </Fragment> + ); + } + return ( + <Fragment> + <EuiSpacer size="s" /> + <EuiCallOut + size="s" + iconType="iInCircle" + data-test-subj="reenterValuesMessage" + title={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel', + { defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' } + )} + /> + <EuiSpacer size="m" /> + </Fragment> + ); +} + +// eslint-disable-next-line import/no-default-export +export { TeamsActionFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.test.tsx new file mode 100644 index 0000000000000..02ad3e33a28e0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { mountWithIntl } from '@kbn/test/jest'; +import TeamsParamsFields from './teams_params'; +import { DocLinksStart } from 'kibana/public'; +import { coreMock } from 'src/core/public/mocks'; + +describe('TeamsParamsFields renders', () => { + test('all params fields is rendered', () => { + const mocks = coreMock.createSetup(); + const actionParams = { + message: 'test message', + }; + + const wrapper = mountWithIntl( + <TeamsParamsFields + actionParams={actionParams} + errors={{ message: [] }} + editAction={() => {}} + index={0} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + toastNotifications={mocks.notifications.toasts} + http={mocks.http} + /> + ); + expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="messageTextArea"]').first().prop('value')).toStrictEqual( + 'test message' + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx new file mode 100644 index 0000000000000..11eb3ec4e318e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { TeamsActionParams } from '../types'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; + +const TeamsParamsFields: React.FunctionComponent<ActionParamsProps<TeamsActionParams>> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + defaultMessage, +}) => { + const { message } = actionParams; + useEffect(() => { + if (!message && defaultMessage && defaultMessage.length > 0) { + editAction('message', defaultMessage, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <TextAreaWithMessageVariables + index={index} + editAction={editAction} + messageVariables={messageVariables} + paramsProperty={'message'} + inputTargetValue={message} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel', + { + defaultMessage: 'Message', + } + )} + errors={errors.message as string[]} + /> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TeamsParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index e22cd268f9bc5..8db7d43f76a84 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -60,6 +60,10 @@ export interface SlackActionParams { message: string; } +export interface TeamsActionParams { + message: string; +} + export interface WebhookActionParams { body?: string; } @@ -119,3 +123,9 @@ export interface WebhookSecrets { } export type WebhookActionConnector = UserConfiguredActionConnector<WebhookConfig, WebhookSecrets>; + +export interface TeamsSecrets { + webhookUrl: string; +} + +export type TeamsActionConnector = UserConfiguredActionConnector<unknown, TeamsSecrets>; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 9e89a38377a4d..7fb50eaab7d7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert, AlertType } from '../../types'; +import { AlertType } from '../../types'; +import { InitialAlert } from '../sections/alert_form/alert_reducer'; /** * NOTE: Applications that want to show the alerting UIs will need to add @@ -21,9 +22,9 @@ export const hasExecuteActionsCapability = (capabilities: Capabilities) => export const hasDeleteActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.delete; -export function hasAllPrivilege(alert: Alert, alertType?: AlertType): boolean { +export function hasAllPrivilege(alert: InitialAlert, alertType?: AlertType): boolean { return alertType?.authorizedConsumers[alert.consumer]?.all ?? false; } -export function hasReadPrivilege(alert: Alert, alertType?: AlertType): boolean { +export function hasReadPrivilege(alert: InitialAlert, alertType?: AlertType): boolean { return alertType?.authorizedConsumers[alert.consumer]?.read ?? false; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 50f5167b9e5c2..83e6386122eb2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -36,7 +36,7 @@ import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants'; -import { ActionGroup } from '../../../../../alerts/common'; +import { ActionGroup, AlertActionParam } from '../../../../../alerts/common'; export interface ActionAccordionFormProps { actions: AlertAction[]; @@ -45,7 +45,7 @@ export interface ActionAccordionFormProps { setActionIdByIndex: (id: string, index: number) => void; setActionGroupIdByIndex?: (group: string, index: number) => void; setAlertProperty: (actions: AlertAction[]) => void; - setActionParamsProperty: (key: string, value: any, index: number) => void; + setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void; http: HttpSetup; actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsSetup; 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 bd40d35b15b2d..5f1798d101d94 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 @@ -25,7 +25,7 @@ import { EuiLoadingSpinner, EuiBadge, } from '@elastic/eui'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; import { IErrorObject, AlertAction, @@ -50,7 +50,7 @@ export type ActionTypeFormProps = { onAddConnector: () => void; onConnectorSelected: (id: string) => void; onDeleteAction: () => void; - setActionParamsProperty: (key: string, value: any, index: number) => void; + setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void; actionTypesIndex: ActionTypeIndex; connectors: ActionConnector[]; } & Pick< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index b38f0e749a28d..d7de7e0a82c1e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -75,8 +75,8 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ chrome, } = useAppDependencies(); const [{}, dispatch] = useReducer(alertReducer, { alert }); - const setInitialAlert = (key: string, value: any) => { - dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); + const setInitialAlert = (value: Alert) => { + dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; // Set breadcrumb and page title @@ -172,7 +172,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ <AlertEdit initialAlert={alert} onClose={() => { - setInitialAlert('alert', alert); + setInitialAlert(alert); setEditFlyoutVisibility(false); }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 741cbadb07070..34a4c909c65a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -3,15 +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 React, { useCallback, useReducer, useState, useEffect } from 'react'; -import { isObject } from 'lodash'; +import React, { useCallback, useReducer, useMemo, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; -import { AlertForm, validateBaseProperties } from './alert_form'; -import { alertReducer } from './alert_reducer'; +import { AlertForm, isValidAlert, validateBaseProperties } from './alert_form'; +import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { ConfirmAlertSave } from './confirm_alert_save'; @@ -36,27 +35,32 @@ export const AlertAdd = ({ alertTypeId, initialValues, }: AlertAddProps) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const initialAlert = ({ - params: {}, - consumer, - alertTypeId, - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - ...(initialValues ? initialValues : {}), - } as unknown) as Alert; - - const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); + const initialAlert: InitialAlert = useMemo( + () => ({ + params: {}, + consumer, + alertTypeId, + schedule: { + interval: '1m', + }, + actions: [], + tags: [], + ...(initialValues ? initialValues : {}), + }), + [alertTypeId, consumer, initialValues] + ); + + const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, { + alert: initialAlert, + }); const [isSaving, setIsSaving] = useState<boolean>(false); const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState<boolean>(false); - const setAlert = (value: any) => { + const setAlert = (value: InitialAlert) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; - const setAlertProperty = (key: string, value: any) => { + + const setAlertProperty = <Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; @@ -73,7 +77,7 @@ export const AlertAdd = ({ const canShowActions = hasShowActionsCapability(capabilities); useEffect(() => { - setAlertProperty('alertTypeId', alertTypeId); + setAlertProperty('alertTypeId', alertTypeId ?? null); }, [alertTypeId]); const closeFlyout = useCallback(() => { @@ -101,7 +105,7 @@ export const AlertAdd = ({ ...(alertType ? alertType.validate(alert.params).errors : []), ...validateBaseProperties(alert).errors, } as IErrorObject; - const hasErrors = parseErrors(errors); + const hasErrors = !isValidAlert(alert, errors); const actionsErrors: Array<{ errors: IErrorObject; @@ -121,16 +125,18 @@ export const AlertAdd = ({ async function onSaveAlert(): Promise<Alert | undefined> { try { - const newAlert = await createAlert({ http, alert }); - toastNotifications.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', { - defaultMessage: 'Created alert "{alertName}"', - values: { - alertName: newAlert.name, - }, - }) - ); - return newAlert; + if (isValidAlert(alert, errors)) { + const newAlert = await createAlert({ http, alert }); + toastNotifications.addSuccess( + i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', { + defaultMessage: 'Created alert "{alertName}"', + values: { + alertName: newAlert.name, + }, + }) + ); + return newAlert; + } } catch (errorRes) { toastNotifications.addDanger( errorRes.body?.message ?? @@ -207,11 +213,5 @@ export const AlertAdd = ({ ); }; -const parseErrors: (errors: IErrorObject) => boolean = (errors) => - !!Object.values(errors).find((errorList) => { - if (isObject(errorList)) return parseErrors(errorList as IErrorObject); - return errorList.length >= 1; - }); - // eslint-disable-next-line import/no-default-export export { AlertAdd as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx new file mode 100644 index 0000000000000..8029b43a2cf53 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; +import { AlertConditions, ActionGroupWithCondition } from './alert_conditions'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiButtonEmpty, +} from '@elastic/eui'; + +describe('alert_conditions', () => { + async function setup(element: React.ReactElement): Promise<ReactWrapper<unknown>> { + const wrapper = mountWithIntl(element); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + return wrapper; + } + + it('renders with custom headline', async () => { + const wrapper = await setup( + <AlertConditions + headline={'Set different threshold with their own status'} + actionGroups={[]} + /> + ); + + expect(wrapper.find(EuiTitle).find(FormattedMessage).prop('id')).toMatchInlineSnapshot( + `"xpack.triggersActionsUI.sections.alertForm.conditions.title"` + ); + expect( + wrapper.find(EuiTitle).find(FormattedMessage).prop('defaultMessage') + ).toMatchInlineSnapshot(`"Conditions:"`); + + expect(wrapper.find('[data-test-subj="alertConditionsHeadline"]').get(0)) + .toMatchInlineSnapshot(` + <EuiText + color="subdued" + data-test-subj="alertConditionsHeadline" + size="s" + > + Set different threshold with their own status + </EuiText> + `); + }); + + it('renders any action group with conditions on it', async () => { + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + <EuiDescriptionList> + <EuiDescriptionListTitle>ID</EuiDescriptionListTitle> + <EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Name</EuiDescriptionListTitle> + <EuiDescriptionListDescription>{actionGroup?.name}</EuiDescriptionListDescription> + <EuiDescriptionListTitle>SomeProp</EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {actionGroup?.conditions?.someProp} + </EuiDescriptionListDescription> + </EuiDescriptionList> + ); + }; + + const wrapper = await setup( + <AlertConditions + actionGroups={[ + { id: 'default', name: 'Default', conditions: { someProp: 'my prop value' } }, + ]} + > + <ConditionForm /> + </AlertConditions> + ); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) + .toMatchInlineSnapshot(` + <EuiDescriptionListDescription> + default + </EuiDescriptionListDescription> + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) + .toMatchInlineSnapshot(` + <EuiDescriptionListDescription> + Default + </EuiDescriptionListDescription> + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(2)) + .toMatchInlineSnapshot(` + <EuiDescriptionListDescription> + my prop value + </EuiDescriptionListDescription> + `); + }); + + it('doesnt render action group without conditions', async () => { + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + <EuiDescriptionList> + <EuiDescriptionListTitle>ID</EuiDescriptionListTitle> + <EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription> + </EuiDescriptionList> + ); + }; + + const wrapper = await setup( + <AlertConditions + actionGroups={[ + { id: 'default', name: 'Default', conditions: { someProp: 'default on a prop' } }, + { + id: 'shouldRender', + name: 'Should Render', + conditions: { someProp: 'shouldRender on a prop' }, + }, + { + id: 'shouldntRender', + name: 'Should Not Render', + }, + ]} + > + <ConditionForm /> + </AlertConditions> + ); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) + .toMatchInlineSnapshot(` + <EuiDescriptionListDescription> + default + </EuiDescriptionListDescription> + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) + .toMatchInlineSnapshot(` + <EuiDescriptionListDescription> + shouldRender + </EuiDescriptionListDescription> + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).length).toEqual(2); + }); + + it('render add buttons for action group without conditions', async () => { + const onInitializeConditionsFor = jest.fn(); + + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + <EuiDescriptionList> + <EuiDescriptionListTitle>ID</EuiDescriptionListTitle> + <EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription> + </EuiDescriptionList> + ); + }; + + const wrapper = await setup( + <AlertConditions + actionGroups={[ + { + id: 'shouldntRenderLink', + name: 'Should Not Render Link', + conditions: { someProp: 'shouldRender on a prop' }, + }, + { + id: 'shouldRenderLink', + name: 'Should Render A Link', + }, + ]} + onInitializeConditionsFor={onInitializeConditionsFor} + > + <ConditionForm /> + </AlertConditions> + ); + + expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(` + <EuiButtonEmpty + flush="left" + onClick={[Function]} + size="s" + > + Should Render A Link + </EuiButtonEmpty> + `); + wrapper.find(EuiButtonEmpty).simulate('click'); + + expect(onInitializeConditionsFor).toHaveBeenCalledWith({ + id: 'shouldRenderLink', + name: 'Should Render A Link', + }); + }); + + it('passes in any additional props the container passes in', async () => { + const callbackProp = jest.fn(); + + const ConditionForm = ({ + actionGroup, + someCallbackProp, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + someCallbackProp: (actionGroup: ActionGroupWithCondition<{ someProp: string }>) => void; + }) => { + if (!actionGroup) { + return <div />; + } + + // call callback when the actionGroup is available + someCallbackProp(actionGroup); + return ( + <EuiDescriptionList> + <EuiDescriptionListTitle>ID</EuiDescriptionListTitle> + <EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Name</EuiDescriptionListTitle> + <EuiDescriptionListDescription>{actionGroup?.name}</EuiDescriptionListDescription> + <EuiDescriptionListTitle>SomeProp</EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {actionGroup?.conditions?.someProp} + </EuiDescriptionListDescription> + </EuiDescriptionList> + ); + }; + + await setup( + <AlertConditions + actionGroups={[ + { id: 'default', name: 'Default', conditions: { someProp: 'my prop value' } }, + ]} + > + <ConditionForm someCallbackProp={callbackProp} /> + </AlertConditions> + ); + + expect(callbackProp).toHaveBeenCalledWith({ + id: 'default', + name: 'Default', + conditions: { someProp: 'my prop value' }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx new file mode 100644 index 0000000000000..1eb086dd6a2c5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { PropsWithChildren } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexItem, EuiText, EuiFlexGroup, EuiTitle, EuiButtonEmpty } from '@elastic/eui'; +import { partition } from 'lodash'; +import { ActionGroup, getBuiltinActionGroups } from '../../../../../alerts/common'; + +const BUILT_IN_ACTION_GROUPS: Set<string> = new Set(getBuiltinActionGroups().map(({ id }) => id)); + +export type ActionGroupWithCondition<T> = ActionGroup & + ( + | // allow isRequired=false with or without conditions + { + conditions?: T; + isRequired?: false; + } + // but if isRequired=true then conditions must be specified + | { + conditions: T; + isRequired: true; + } + ); + +export interface AlertConditionsProps<ConditionProps> { + headline?: string; + actionGroups: Array<ActionGroupWithCondition<ConditionProps>>; + onInitializeConditionsFor?: (actionGroup: ActionGroupWithCondition<ConditionProps>) => void; + onResetConditionsFor?: (actionGroup: ActionGroupWithCondition<ConditionProps>) => void; + includeBuiltInActionGroups?: boolean; +} + +export const AlertConditions = <ConditionProps extends any>({ + headline, + actionGroups, + onInitializeConditionsFor, + onResetConditionsFor, + includeBuiltInActionGroups = false, + children, +}: PropsWithChildren<AlertConditionsProps<ConditionProps>>) => { + const [withConditions, withoutConditions] = partition( + includeBuiltInActionGroups + ? actionGroups + : actionGroups.filter(({ id }) => !BUILT_IN_ACTION_GROUPS.has(id)), + (actionGroup) => actionGroup.hasOwnProperty('conditions') + ); + + return ( + <EuiFlexGroup direction="column" gutterSize="s"> + <EuiFlexItem> + <EuiTitle size="s"> + <EuiFlexGroup component="span" alignItems="baseline"> + <EuiFlexItem grow={false}> + <h6 className="alertConditions"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertForm.conditions.title" + defaultMessage="Conditions:" + /> + </h6> + </EuiFlexItem> + {headline && ( + <EuiFlexItem> + <EuiText color="subdued" size="s" data-test-subj="alertConditionsHeadline"> + {headline} + </EuiText> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup direction="column"> + {withConditions.map((actionGroup) => ( + <EuiFlexItem key={`condition-${actionGroup.id}`}> + {React.isValidElement(children) && + React.cloneElement( + React.Children.only(children), + onResetConditionsFor + ? { + actionGroup, + onResetConditionsFor, + } + : { actionGroup } + )} + </EuiFlexItem> + ))} + {onInitializeConditionsFor && withoutConditions.length > 0 && ( + <EuiFlexItem> + <EuiFlexGroup direction="row" alignItems="baseline"> + <EuiFlexItem grow={false}> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertForm.conditions.addConditionLabel" + defaultMessage="Add:" + /> + </EuiFlexItem> + {withoutConditions.map((actionGroup) => ( + <EuiFlexItem key={`condition-add-${actionGroup.id}`} grow={false}> + <EuiButtonEmpty + flush="left" + size="s" + onClick={() => onInitializeConditionsFor(actionGroup)} + > + {actionGroup.name} + </EuiButtonEmpty> + </EuiFlexItem> + ))} + </EuiFlexGroup> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx new file mode 100644 index 0000000000000..dd12af4ae9e62 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; +import { AlertConditionsGroup } from './alert_conditions_group'; +import { EuiFormRow, EuiButtonIcon } from '@elastic/eui'; + +describe('alert_conditions_group', () => { + async function setup(element: React.ReactElement): Promise<ReactWrapper<unknown>> { + const wrapper = mountWithIntl(element); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + return wrapper; + } + + it('renders with actionGroup name as label', async () => { + const InnerComponent = () => <div>{'inner component'}</div>; + const wrapper = await setup( + <AlertConditionsGroup + actionGroup={{ + id: 'myGroup', + name: 'My Group', + }} + > + <InnerComponent /> + </AlertConditionsGroup> + ); + + expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(` + <EuiTitle + size="s" + > + <strong> + My Group + </strong> + </EuiTitle> + `); + expect(wrapper.find(InnerComponent).prop('actionGroup')).toMatchInlineSnapshot(` + Object { + "id": "myGroup", + "name": "My Group", + } + `); + }); + + it('renders a reset button when onResetConditionsFor is specified', async () => { + const onResetConditionsFor = jest.fn(); + const wrapper = await setup( + <AlertConditionsGroup + actionGroup={{ + id: 'myGroup', + name: 'My Group', + }} + onResetConditionsFor={onResetConditionsFor} + > + <div>{'inner component'}</div> + </AlertConditionsGroup> + ); + + expect(wrapper.find(EuiButtonIcon).prop('aria-label')).toMatchInlineSnapshot(`"Remove"`); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(onResetConditionsFor).toHaveBeenCalledWith({ + id: 'myGroup', + name: 'My Group', + }); + }); + + it('shouldnt render a reset button when isRequired is true', async () => { + const onResetConditionsFor = jest.fn(); + const wrapper = await setup( + <AlertConditionsGroup + actionGroup={{ + id: 'myGroup', + name: 'My Group', + conditions: true, + isRequired: true, + }} + onResetConditionsFor={onResetConditionsFor} + > + <div>{'inner component'}</div> + </AlertConditionsGroup> + ); + + expect(wrapper.find(EuiButtonIcon).length).toEqual(0); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx new file mode 100644 index 0000000000000..879f276317503 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment, PropsWithChildren } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui'; +import { AlertConditionsProps, ActionGroupWithCondition } from './alert_conditions'; + +export type AlertConditionsGroupProps<ConditionProps> = { + actionGroup?: ActionGroupWithCondition<ConditionProps>; +} & Pick<AlertConditionsProps<ConditionProps>, 'onResetConditionsFor'>; + +export const AlertConditionsGroup = <ConditionProps extends unknown>({ + actionGroup, + onResetConditionsFor, + children, + ...otherProps +}: PropsWithChildren<AlertConditionsGroupProps<ConditionProps>>) => { + if (!actionGroup) { + return null; + } + + return ( + <EuiFormRow + label={ + <EuiTitle size="s"> + <strong>{actionGroup.name}</strong> + </EuiTitle> + } + fullWidth + labelAppend={ + onResetConditionsFor && + !actionGroup.isRequired && ( + <EuiButtonIcon + iconType="minusInCircle" + color="danger" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel', + { + defaultMessage: 'Remove', + } + )} + onClick={() => onResetConditionsFor(actionGroup)} + /> + ) + } + > + {React.isValidElement(children) ? ( + React.cloneElement(React.Children.only(children), { + actionGroup, + ...otherProps, + }) + ) : ( + <Fragment /> + )} + </EuiFormRow> + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index d5ae701546c64..2e2a77fa6afc3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; -import { alertReducer } from './alert_reducer'; +import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { HealthContextProvider } from '../../context/health_context'; @@ -34,7 +34,9 @@ interface AlertEditProps { } export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { - const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); + const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, { + alert: initialAlert, + }); const [isSaving, setIsSaving] = useState<boolean>(false); const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false); const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState<boolean>( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c571520988509..b06fb3c39ea45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -33,14 +33,14 @@ import { } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; -import { capitalize } from 'lodash'; +import { capitalize, isObject } from 'lodash'; import { KibanaFeature } from '../../../../../features/public'; import { getDurationNumberInItsUnit, getDurationUnitValue, } from '../../../../../alerts/common/parse_duration'; import { loadAlertTypes } from '../../lib/alert_api'; -import { AlertReducerAction } from './alert_reducer'; +import { AlertReducerAction, InitialAlert } from './alert_reducer'; import { AlertTypeModel, Alert, @@ -48,18 +48,19 @@ import { AlertAction, AlertTypeIndex, AlertType, + ValidationResult, } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; -import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; +import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common'; import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; import { SolutionFilter } from './solution_filter'; import './alert_form.scss'; const ENTER_KEY = 13; -export function validateBaseProperties(alertObject: Alert) { +export function validateBaseProperties(alertObject: InitialAlert): ValidationResult { const validationResult = { errors: {} }; const errors = { name: new Array<string>(), @@ -92,12 +93,25 @@ export function validateBaseProperties(alertObject: Alert) { return validationResult; } +const hasErrors: (errors: IErrorObject) => boolean = (errors) => + !!Object.values(errors).find((errorList) => { + if (isObject(errorList)) return hasErrors(errorList as IErrorObject); + return errorList.length >= 1; + }); + +export function isValidAlert( + alertObject: InitialAlert | Alert, + validationResult: IErrorObject +): alertObject is Alert { + return !hasErrors(validationResult); +} + function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } interface AlertFormProps { - alert: Alert; + alert: InitialAlert; dispatch: React.Dispatch<AlertReducerAction>; errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button @@ -203,10 +217,13 @@ export const AlertForm = ({ useEffect(() => { setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null); - }, [alert, alertTypeRegistry]); + if (alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId)) { + setDefaultActionGroupId(alertTypesIndex.get(alert.alertTypeId)!.defaultActionGroupId); + } + }, [alert, alert.alertTypeId, alertTypesIndex, alertTypeRegistry]); const setAlertProperty = useCallback( - (key: string, value: any) => { + <Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }, [dispatch] @@ -225,12 +242,16 @@ export const AlertForm = ({ dispatch({ command: { type: 'setScheduleProperty' }, payload: { key, value } }); }; - const setActionProperty = (key: string, value: any, index: number) => { + const setActionProperty = <Key extends keyof AlertAction>( + key: Key, + value: AlertAction[Key] | null, + index: number + ) => { dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } }); }; const setActionParamsProperty = useCallback( - (key: string, value: any, index: number) => { + (key: string, value: AlertActionParam, index: number) => { dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } }); }, [dispatch] @@ -436,7 +457,10 @@ export const AlertForm = ({ </EuiFlexGroup> )} <EuiHorizontalRule /> - {AlertParamsExpressionComponent ? ( + {AlertParamsExpressionComponent && + defaultActionGroupId && + alert.alertTypeId && + alertTypesIndex?.has(alert.alertTypeId) ? ( <Suspense fallback={<CenterJustifiedSpinner />}> <AlertParamsExpressionComponent alertParams={alert.params} @@ -446,12 +470,15 @@ export const AlertForm = ({ setAlertParams={setAlertParams} setAlertProperty={setAlertProperty} alertsContext={alertsContext} + defaultActionGroupId={defaultActionGroupId} + actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups} /> </Suspense> ) : null} {canShowActions && defaultActionGroupId && alertTypeModel && + alert.alertTypeId && alertTypesIndex?.has(alert.alertTypeId) ? ( <ActionForm actions={alert.actions} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts index 2e56f4b026b4a..e54895318fc70 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts @@ -3,38 +3,93 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectAttribute } from 'kibana/public'; import { isEqual } from 'lodash'; +import { Reducer } from 'react'; +import { AlertActionParam, IntervalSchedule } from '../../../../../alerts/common'; +import { Alert, AlertAction } from '../../../types'; -interface CommandType { - type: +export type InitialAlert = Partial<Alert> & + Pick<Alert, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags'>; + +interface CommandType< + T extends | 'setAlert' | 'setProperty' | 'setScheduleProperty' | 'setAlertParams' | 'setAlertActionParams' - | 'setAlertActionProperty'; + | 'setAlertActionProperty' +> { + type: T; } export interface AlertState { - alert: any; + alert: InitialAlert; +} + +interface Payload<Keys, Value> { + key: Keys; + value: Value; + index?: number; +} + +interface AlertPayload<Key extends keyof Alert> { + key: Key; + value: Alert[Key] | null; + index?: number; +} + +interface AlertActionPayload<Key extends keyof AlertAction> { + key: Key; + value: AlertAction[Key] | null; + index?: number; } -export interface AlertReducerAction { - command: CommandType; - payload: { - key: string; - value: {}; - index?: number; - }; +interface AlertSchedulePayload<Key extends keyof IntervalSchedule> { + key: Key; + value: IntervalSchedule[Key]; + index?: number; } -export const alertReducer = (state: any, action: AlertReducerAction) => { - const { command, payload } = action; +export type AlertReducerAction = + | { + command: CommandType<'setAlert'>; + payload: Payload<'alert', InitialAlert>; + } + | { + command: CommandType<'setProperty'>; + payload: AlertPayload<keyof Alert>; + } + | { + command: CommandType<'setScheduleProperty'>; + payload: AlertSchedulePayload<keyof IntervalSchedule>; + } + | { + command: CommandType<'setAlertParams'>; + payload: Payload<string, unknown>; + } + | { + command: CommandType<'setAlertActionParams'>; + payload: Payload<string, AlertActionParam>; + } + | { + command: CommandType<'setAlertActionProperty'>; + payload: AlertActionPayload<keyof AlertAction>; + }; + +export type InitialAlertReducer = Reducer<{ alert: InitialAlert }, AlertReducerAction>; +export type ConcreteAlertReducer = Reducer<{ alert: Alert }, AlertReducerAction>; + +export const alertReducer = <AlertPhase extends InitialAlert | Alert>( + state: { alert: AlertPhase }, + action: AlertReducerAction +) => { const { alert } = state; - switch (command.type) { + switch (action.command.type) { case 'setAlert': { - const { key, value } = payload; + const { key, value } = action.payload as Payload<'alert', AlertPhase>; if (key === 'alert') { return { ...state, @@ -45,7 +100,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setProperty': { - const { key, value } = payload; + const { key, value } = action.payload as AlertPayload<keyof Alert>; if (isEqual(alert[key], value)) { return state; } else { @@ -59,8 +114,8 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setScheduleProperty': { - const { key, value } = payload; - if (isEqual(alert.schedule[key], value)) { + const { key, value } = action.payload as AlertSchedulePayload<keyof IntervalSchedule>; + if (alert.schedule && isEqual(alert.schedule[key], value)) { return state; } else { return { @@ -76,7 +131,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setAlertParams': { - const { key, value } = payload; + const { key, value } = action.payload as Payload<string, Record<string, unknown>>; if (isEqual(alert.params[key], value)) { return state; } else { @@ -93,7 +148,10 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setAlertActionParams': { - const { key, value, index } = payload; + const { key, value, index } = action.payload as Payload< + keyof AlertAction, + SavedObjectAttribute + >; if (index === undefined || isEqual(alert.actions[index][key], value)) { return state; } else { @@ -116,7 +174,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setAlertActionProperty': { - const { key, value, index } = payload; + const { key, value, index } = action.payload as AlertActionPayload<keyof AlertAction>; if (index === undefined || isEqual(alert.actions[index][key], value)) { return state; } else { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx index 79720edc4672e..421f0fc26dd68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx @@ -5,6 +5,12 @@ */ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +export { + AlertConditions, + ActionGroupWithCondition, + AlertConditionsProps, +} from './alert_conditions'; +export { AlertConditionsGroup } from './alert_conditions_group'; export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 677ee139271c0..490aeb5be8bd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -6,6 +6,12 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../lib/suspended_component_with_props'; +export { + ActionGroupWithCondition, + AlertConditionsProps, + AlertConditions, + AlertConditionsGroup, +} from './alert_form'; export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index c479359ff7e6e..025741aa7f9bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -9,7 +9,12 @@ import { Plugin } from './plugin'; export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; -export { AlertEdit } from './application/sections'; +export { + AlertEdit, + AlertConditions, + AlertConditionsGroup, + ActionGroupWithCondition, +} from './application/sections'; export { ActionForm } from './application/sections/action_connector_form'; export { AlertAction, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 16c6bbc215ddc..cc0522eeb52a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -6,7 +6,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { HttpSetup, DocLinksStart, ToastsSetup } from 'kibana/public'; import { ComponentType } from 'react'; -import { ActionGroup } from '../../alerts/common'; +import { ActionGroup, AlertActionParam } from '../../alerts/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; import { @@ -52,7 +52,7 @@ export interface ActionConnectorFieldsProps<TActionConnector> { export interface ActionParamsProps<TParams> { actionParams: TParams; index: number; - editAction: (property: string, value: any, index: number) => void; + editAction: (key: string, value: AlertActionParam, index: number) => void; errors: IErrorObject; messageVariables?: ActionVariable[]; defaultMessage?: string; @@ -166,9 +166,11 @@ export interface AlertTypeParamsExpressionProps< alertInterval: string; alertThrottle: string; setAlertParams: (property: string, value: any) => void; - setAlertProperty: (key: string, value: any) => void; + setAlertProperty: <Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => void; errors: IErrorObject; alertsContext: AlertsContextValue; + defaultActionGroupId: string; + actionGroups: ActionGroup[]; } export interface AlertTypeModel<AlertParamsType = any, AlertsContextValue = any> { diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index fc9db4a8b6b22..79ab7943d72a7 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -20,7 +20,6 @@ "home", "data", "ml", - "apm", "maps" ] } diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx index 35335f9868978..5195eef6e9a3b 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx @@ -27,6 +27,7 @@ import { ChartEmptyState } from './chart_empty_state'; import { DurationAnomaliesBar } from './duration_line_bar_list'; import { AnomalyRecords } from '../../../state/actions'; import { UptimeThemeContext } from '../../../contexts'; +import { MONITOR_CHART_HEIGHT } from '../../monitor'; interface DurationChartProps { /** @@ -86,7 +87,7 @@ export const DurationChartComponent = ({ }; return ( - <ChartWrapper height="400px" loading={loading}> + <ChartWrapper height={MONITOR_CHART_HEIGHT} loading={loading}> {hasLines ? ( <Chart> <Settings diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx index 552c2e587e3d3..1eeaebc448d64 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx @@ -12,6 +12,7 @@ import { MonitorDuration } from './monitor_duration/monitor_duration_container'; interface MonitorChartsProps { monitorId: string; } +export const MONITOR_CHART_HEIGHT = '248px'; export const MonitorCharts = ({ monitorId }: MonitorChartsProps) => { return ( @@ -20,7 +21,7 @@ export const MonitorCharts = ({ monitorId }: MonitorChartsProps) => { <MonitorDuration monitorId={monitorId} /> </EuiFlexItem> <EuiFlexItem> - <PingHistogram height="400px" isResponsive={false} /> + <PingHistogram height={MONITOR_CHART_HEIGHT} isResponsive={false} /> </EuiFlexItem> </EuiFlexGroup> ); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx index dcd8df1ba18ef..4e2b08d97cf4b 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx @@ -10,10 +10,26 @@ import { isEmpty } from 'lodash'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { Suggestion } from './suggestion'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { units, px, unit } from '../../../../../../apm/public/style/variables'; import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; +export const unit = 16; + +export const units = { + unit, + eighth: unit / 8, + quarter: unit / 4, + half: unit / 2, + minus: unit * 0.75, + plus: unit * 1.5, + double: unit * 2, + triple: unit * 3, + quadruple: unit * 4, +}; + +export function px(value: number): string { + return `${value}px`; +} + const List = styled.ul` width: 100%; border: 1px solid ${theme.euiColorLightShade}; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 7d4cc41cfbe5a..505ad3c7d866b 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -43,6 +43,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/security_api_integration/oidc.config.ts'), require.resolve('../test/security_api_integration/oidc_implicit_flow.config.ts'), require.resolve('../test/security_api_integration/token.config.ts'), + require.resolve('../test/security_api_integration/anonymous.config.ts'), require.resolve('../test/observability_api_integration/basic/config.ts'), require.resolve('../test/observability_api_integration/trial/config.ts'), require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'), diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 969f291b0d8b3..52ae28d75cc17 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -23,7 +23,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.securityCommon.cleanMlRoles(); await ml.testResources.deleteIndexPatternByTitle('ft_module_apache'); + await ml.testResources.deleteIndexPatternByTitle('ft_module_auditbeat'); await ml.testResources.deleteIndexPatternByTitle('ft_module_apm'); + await ml.testResources.deleteIndexPatternByTitle('ft_module_heartbeat'); await ml.testResources.deleteIndexPatternByTitle('ft_module_logs'); await ml.testResources.deleteIndexPatternByTitle('ft_module_nginx'); await ml.testResources.deleteIndexPatternByTitle('ft_module_sample_ecommerce'); @@ -36,7 +38,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('ml/ecommerce'); await esArchiver.unload('ml/categorization'); await esArchiver.unload('ml/module_apache'); + await esArchiver.unload('ml/module_auditbeat'); await esArchiver.unload('ml/module_apm'); + await esArchiver.unload('ml/module_heartbeat'); await esArchiver.unload('ml/module_logs'); await esArchiver.unload('ml/module_nginx'); await esArchiver.unload('ml/module_sample_ecommerce'); diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index d50148ec583a0..d327a27bc9821 100644 --- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -115,6 +115,26 @@ export default ({ getService }: FtrProviderContext) => { moduleIds: [], }, }, + { + testTitleSuffix: 'for heartbeat dataset', + sourceDataArchive: 'ml/module_heartbeat', + indexPattern: 'ft_module_heartbeat', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: ['uptime_heartbeat'], + }, + }, + { + testTitleSuffix: 'for auditbeat dataset', + sourceDataArchive: 'ml/module_auditbeat', + indexPattern: 'ft_module_auditbeat', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: ['auditbeat_process_hosts_ecs', 'siem_auditbeat'], + }, + }, ]; async function executeRecognizeModuleRequest(indexPattern: string, user: USER, rspCode: number) { diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index fcf4c8d0c328f..c86cd8400a71a 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -451,6 +451,75 @@ export default ({ getService }: FtrProviderContext) => { dashboards: [] as string[], }, }, + { + testTitleSuffix: + 'for uptime_heartbeat with prefix, startDatafeed true and estimateModelMemory true', + sourceDataArchive: 'ml/module_heartbeat', + indexPattern: { name: 'ft_module_heartbeat', timeField: '@timestamp' }, + module: 'uptime_heartbeat', + user: USER.ML_POWERUSER, + requestBody: { + prefix: 'pf13_', + indexPatternName: 'ft_module_heartbeat', + startDatafeed: true, + end: Date.now(), + }, + expected: { + responseCode: 200, + jobs: [ + { + jobId: 'pf13_high_latency_by_geo', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + ], + searches: [] as string[], + visualizations: [] as string[], + dashboards: [] as string[], + }, + }, + { + testTitleSuffix: + 'for auditbeat_process_hosts_ecs with prefix, startDatafeed true and estimateModelMemory true', + sourceDataArchive: 'ml/module_auditbeat', + indexPattern: { name: 'ft_module_auditbeat', timeField: '@timestamp' }, + module: 'auditbeat_process_hosts_ecs', + user: USER.ML_POWERUSER, + requestBody: { + prefix: 'pf14_', + indexPatternName: 'ft_module_auditbeat', + startDatafeed: true, + end: Date.now(), + }, + expected: { + responseCode: 200, + jobs: [ + { + jobId: 'pf14_hosts_high_count_process_events_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf14_hosts_rare_process_activity_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + ], + searches: ['ml_auditbeat_hosts_process_events_ecs'] as string[], + visualizations: [ + 'ml_auditbeat_hosts_process_event_rate_by_process_ecs', + 'ml_auditbeat_hosts_process_event_rate_vis_ecs', + 'ml_auditbeat_hosts_process_occurrence_ecs', + ] as string[], + dashboards: [ + 'ml_auditbeat_hosts_process_event_rate_ecs', + 'ml_auditbeat_hosts_process_explorer_ecs', + ] as string[], + }, + }, ]; const testDataListNegative = [ diff --git a/x-pack/test/api_integration/apis/security_solution/feature_controls.ts b/x-pack/test/api_integration/apis/security_solution/feature_controls.ts index c2dfd28d5c844..0137a90ce9817 100644 --- a/x-pack/test/api_integration/apis/security_solution/feature_controls.ts +++ b/x-pack/test/api_integration/apis/security_solution/feature_controls.ts @@ -82,10 +82,11 @@ export default function ({ getService }: FtrProviderContext) { }; describe('feature controls', () => { - let isProd = false; + let isProdOrCi = false; before(() => { const kbnConfig = config.get('servers.kibana'); - isProd = kbnConfig.hostname === 'localhost' && kbnConfig.port === 5620 ? false : true; + isProdOrCi = + !!process.env.CI || !(kbnConfig.hostname === 'localhost' && kbnConfig.port === 5620); }); it(`APIs can't be accessed by user with no privileges`, async () => { const username = 'logstash_read'; @@ -135,7 +136,7 @@ export default function ({ getService }: FtrProviderContext) { expectGraphQLResponse(graphQLResult); const graphQLIResult = await executeGraphIQLRequest(username, password); - if (!isProd) { + if (!isProdOrCi) { expectGraphIQLResponse(graphQLIResult); } else { expectGraphIQL404(graphQLIResult); @@ -234,7 +235,7 @@ export default function ({ getService }: FtrProviderContext) { expectGraphQLResponse(graphQLResult); const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id); - if (!isProd) { + if (!isProdOrCi) { expectGraphIQLResponse(graphQLIResult); } else { expectGraphIQL404(graphQLIResult); diff --git a/x-pack/test/api_integration/services/usage_api.ts b/x-pack/test/api_integration/services/usage_api.ts index c56de5127f743..b4adc6c61b664 100644 --- a/x-pack/test/api_integration/services/usage_api.ts +++ b/x-pack/test/api_integration/services/usage_api.ts @@ -40,7 +40,7 @@ export function UsageAPIProvider({ getService }: FtrProviderContext) { async getTelemetryStats(payload: { unencrypted?: boolean; timestamp: number | string; - }): Promise<TelemetryCollectionManagerPlugin['getStats']> { + }): Promise<ReturnType<TelemetryCollectionManagerPlugin['getStats']>> { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts deleted file mode 100644 index 751ee8753c449..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import archives_metadata from '../../../common/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - // url parameters - const start = '2020-09-29T14:45:00.000Z'; - const end = range.end; - const fieldNames = - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name'; - - describe('Ranges', () => { - const url = format({ - pathname: `/api/apm/correlations/ranges`, - query: { start, end, fieldNames }, - }); - - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - describe('when data is loaded', () => { - let response: PromiseReturnType<typeof supertest.get>; - before(async () => { - await esArchiver.load(archiveName); - response = await supertest.get(url); - }); - - after(() => esArchiver.unload(archiveName)); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns fields in response', () => { - expectSnapshot(Object.keys(response.body.response)).toMatchInline(` - Array [ - "service.node.name", - "host.ip", - "user.id", - "user_agent.name", - "container.id", - "url.domain", - ] - `); - }); - - it('returns cardinality for each field', () => { - const cardinalitys = Object.values(response.body.response).map( - (field: any) => field.cardinality - ); - - expectSnapshot(cardinalitys).toMatchInline(` - Array [ - 5, - 6, - 20, - 6, - 5, - 4, - ] - `); - }); - - it('returns buckets', () => { - const { buckets } = response.body.response['user.id'].value; - expectSnapshot(buckets[0]).toMatchInline(` - Object { - "bg_count": 2, - "doc_count": 7, - "key": "20", - "score": 3.5, - } - `); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts deleted file mode 100644 index 3cf1c2cecb42b..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import archives_metadata from '../../../common/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - // url parameters - const start = range.start; - const end = range.end; - const durationPercentile = 95; - const fieldNames = - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name'; - - // Failing: See https://github.com/elastic/kibana/issues/81264 - describe('Slow durations', () => { - const url = format({ - pathname: `/api/apm/correlations/slow_durations`, - query: { start, end, durationPercentile, fieldNames }, - }); - - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('making request with default args', () => { - let response: PromiseReturnType<typeof supertest.get>; - before(async () => { - response = await supertest.get(url); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns fields in response', () => { - expectSnapshot(Object.keys(response.body.response)).toMatchInline(` - Array [ - "service.node.name", - "host.ip", - "user.id", - "user_agent.name", - "container.id", - "url.domain", - ] - `); - }); - - it('returns cardinality for each field', () => { - const cardinalitys = Object.values(response.body.response).map( - (field: any) => field.cardinality - ); - - expectSnapshot(cardinalitys).toMatchInline(` - Array [ - 5, - 6, - 3, - 5, - 5, - 4, - ] - `); - }); - - it('returns buckets', () => { - const { buckets } = response.body.response['user.id'].value; - expectSnapshot(buckets[0]).toMatchInline(` - Object { - "bg_count": 32, - "doc_count": 6, - "key": "2", - "score": 0.1875, - } - `); - }); - }); - }); - - describe('making a request for each "scoring"', () => { - ['percentage', 'jlh', 'chi_square', 'gnd'].map(async (scoring) => { - it(`returns response for scoring "${scoring}"`, async () => { - const response = await supertest.get( - format({ - pathname: `/api/apm/correlations/slow_durations`, - query: { start, end, durationPercentile, fieldNames, scoring }, - }) - ); - - expect(response.status).to.be(200); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts new file mode 100644 index 0000000000000..c0978db69a3c9 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { format } from 'url'; +import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../../common/archives_metadata'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + describe('Slow durations', () => { + const url = format({ + pathname: `/api/apm/correlations/slow_transactions`, + query: { + start: range.start, + end: range.end, + durationPercentile: 95, + fieldNames: + 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name', + }, + }); + + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + describe('making request with default args', () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + let response: { + status: number; + body: NonNullable<ResponseBody>; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + expectSnapshot(response.body?.significantTerms?.map((term) => term.fieldName)) + .toMatchInline(` + Array [ + "host.ip", + "service.node.name", + "container.id", + "url.domain", + "user_agent.name", + "user.id", + "host.ip", + "service.node.name", + "container.id", + "user.id", + ] + `); + }); + + it('returns a timeseries per term', () => { + // @ts-ignore + expectSnapshot(response.body?.significantTerms[0].timeseries.length).toMatchInline(`31`); + }); + + it('returns a distribution per term', () => { + // @ts-ignore + expectSnapshot(response.body?.significantTerms[0].distribution.length).toMatchInline( + `11` + ); + }); + + it('returns overall timeseries', () => { + // @ts-ignore + expectSnapshot(response.body?.overall.timeseries.length).toMatchInline(`31`); + }); + + it('returns overall distribution', () => { + // @ts-ignore + expectSnapshot(response.body?.overall.distribution.length).toMatchInline(`11`); + }); + }); + }); + }); +} 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 0381e5f51bb9b..e9bc59df96108 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -24,6 +24,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('Service overview', function () { loadTestFile(require.resolve('./service_overview/error_groups')); + loadTestFile(require.resolve('./service_overview/transaction_groups')); }); describe('Settings', function () { @@ -59,8 +60,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont }); describe('Correlations', function () { - loadTestFile(require.resolve('./correlations/slow_durations')); - loadTestFile(require.resolve('./correlations/ranges')); + loadTestFile(require.resolve('./correlations/slow_transactions')); }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts index 088b7cb8bb568..6d0d1e4b52bec 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -99,9 +99,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); - expectSnapshot( - firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length - ).toMatchInline(`7`); + const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0); + expectSnapshot(visibleDataPoints.length).toMatchInline(`7`); }); it('sorts items in the correct order', async () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/transaction_groups.ts new file mode 100644 index 0000000000000..f9ae8cc9a1976 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/transaction_groups.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 expect from '@kbn/expect'; +import { pick, uniqBy } from 'lodash'; +import url from 'url'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview transaction groups', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + totalTransactionGroups: 0, + transactionGroups: [], + isAggregationAccurate: true, + }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body.totalTransactionGroups).toMatchInline(`12`); + + expectSnapshot(response.body.transactionGroups.map((group: any) => group.name)) + .toMatchInline(` + Array [ + "DispatcherServlet#doGet", + "APIRestController#stats", + "APIRestController#topProducts", + "APIRestController#order", + "APIRestController#customer", + ] + `); + + expectSnapshot(response.body.transactionGroups.map((group: any) => group.impact)) + .toMatchInline(` + Array [ + 100, + 0.794579770440557, + 0.298214689777379, + 0.290932594821871, + 0.270655974123907, + ] + `); + + const firstItem = response.body.transactionGroups[0]; + + expectSnapshot( + pick(firstItem, 'name', 'latency.value', 'throughput.value', 'errorRate.value', 'impact') + ).toMatchInline(` + Object { + "errorRate": Object { + "value": 0.107142857142857, + }, + "impact": 100, + "latency": Object { + "value": 996636.214285714, + }, + "name": "DispatcherServlet#doGet", + "throughput": Object { + "value": 28, + }, + } + `); + + expectSnapshot( + firstItem.latency.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`15`); + + expectSnapshot( + firstItem.throughput.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`15`); + + expectSnapshot( + firstItem.errorRate.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`3`); + }); + + it('sorts items in the correct order', async () => { + const descendingResponse = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(descendingResponse.status).to.be(200); + + const descendingOccurrences = descendingResponse.body.transactionGroups.map( + (item: any) => item.impact + ); + + expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); + + const ascendingResponse = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + const ascendingOccurrences = ascendingResponse.body.transactionGroups.map( + (item: any) => item.impact + ); + + expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); + }); + + it('sorts items by the correct field', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'latency', + }, + }) + ); + + expect(response.status).to.be(200); + + const latencies = response.body.transactionGroups.map((group: any) => group.latency.value); + + expect(latencies).to.eql(latencies.concat().sort().reverse()); + }); + + it('paginates through the items', async () => { + const size = 1; + + const firstPage = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(firstPage.status).to.eql(200); + + const totalItems = firstPage.body.totalTransactionGroups; + + const pages = Math.floor(totalItems / size); + + const items = await new Array(pages) + .fill(undefined) + .reduce(async (prevItemsPromise, _, pageIndex) => { + const prevItems = await prevItemsPromise; + + const thisPage = await supertest.get( + url.format({ + pathname: '/api/apm/services/opbeans-java/overview_transaction_groups', + query: { + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + return prevItems.concat(thisPage.body.transactionGroups); + }, Promise.resolve([])); + + expect(items.length).to.eql(totalItems); + + expect(uniqBy(items, 'name').length).to.eql(totalItems); + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index 5fb6f21c51c95..6ab29ffa09e13 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -33,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body: comment } = await supertest @@ -55,7 +55,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body } = await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index c67eda1d3a16b..180fc62d3d39a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { CommentType } from '../../../../../../plugins/case/common/api'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -34,13 +35,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body: caseComments } = await supertest @@ -63,13 +64,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send({ comment: 'unique', type: 'user' }) + .send({ comment: 'unique', type: CommentType.user }) .expect(200); const { body: caseComments } = await supertest @@ -91,7 +92,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 9c3a85e99c29d..e77405f3cd49b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -33,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body: comment } = await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index 3176841b009d4..ca24f0d2e32c5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { defaultUser, postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { CommentType } from '../../../../../../plugins/case/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, +} from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -33,7 +40,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -44,10 +51,43 @@ export default ({ getService }: FtrProviderContext): void => { id: patchedCase.comments[0].id, version: patchedCase.comments[0].version, comment: newComment, + type: CommentType.user, }) .expect(200); expect(body.comments[0].comment).to.eql(newComment); + expect(body.comments[0].type).to.eql('user'); + expect(body.updated_by).to.eql(defaultUser); + }); + + it('should patch an alert', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); + + const { body } = await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + }) + .expect(200); + + expect(body.comments[0].alertId).to.eql('new-id'); + expect(body.comments[0].index).to.eql(postCommentAlertReq.index); + expect(body.comments[0].type).to.eql('alert'); expect(body.updated_by).to.eql(defaultUser); }); @@ -64,6 +104,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ id: 'id', version: 'version', + type: CommentType.user, comment: 'comment', }) .expect(404); @@ -76,12 +117,39 @@ export default ({ getService }: FtrProviderContext): void => { .send({ id: 'id', version: 'version', + type: CommentType.user, comment: 'comment', }) .expect(404); }); - it('unhappy path - 400s when patch body is bad', async () => { + it('unhappy path - 400s when trying to change comment type', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + }) + .expect(400); + }); + + it('unhappy path - 400s when missing attributes for type user', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -91,7 +159,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); await supertest @@ -100,11 +168,100 @@ export default ({ getService }: FtrProviderContext): void => { .send({ id: patchedCase.comments[0].id, version: patchedCase.comments[0].version, - comment: true, }) .expect(400); }); + it('unhappy path - 400s when adding excess attributes for type user', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + for (const attribute of ['alertId', 'index']) { + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: 'a comment', + type: CommentType.user, + [attribute]: attribute, + }) + .expect(400); + } + }); + + it('unhappy path - 400s when missing attributes for type alert', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + ...requestAttributes, + }) + .expect(400); + } + }); + + it('unhappy path - 400s when adding excess attributes for type alert', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + for (const attribute of ['comment']) { + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + [attribute]: attribute, + }) + .expect(400); + } + }); + it('unhappy path - 409s when conflict', async () => { const { body: postedCase } = await supertest .post(CASES_URL) @@ -115,7 +272,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -125,6 +282,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ id: patchedCase.comments[0].id, version: 'version-mismatch', + type: CommentType.user, comment: newComment, }) .expect(409); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index 0c7ab52abf8c8..d26e31394b9f5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { defaultUser, postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { CommentType } from '../../../../../../plugins/case/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, +} from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -33,14 +40,50 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); - expect(patchedCase.comments[0].comment).to.eql(postCommentReq.comment); + expect(patchedCase.comments[0].type).to.eql(postCommentUserReq.type); + expect(patchedCase.comments[0].comment).to.eql(postCommentUserReq.comment); expect(patchedCase.updated_by).to.eql(defaultUser); }); - it('unhappy path - 400s when post body is bad', async () => { + it('should post an alert', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); + + expect(patchedCase.comments[0].type).to.eql(postCommentAlertReq.type); + expect(patchedCase.comments[0].alertId).to.eql(postCommentAlertReq.alertId); + expect(patchedCase.comments[0].index).to.eql(postCommentAlertReq.index); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('unhappy path - 400s when type is missing', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + bad: 'comment', + }) + .expect(400); + }); + + it('unhappy path - 400s when missing attributes for type user', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -50,6 +93,74 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') + .send({ type: CommentType.user }) + .expect(400); + }); + + it('unhappy path - 400s when adding excess attributes for type user', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + for (const attribute of ['alertId', 'index']) { + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ type: CommentType.user, [attribute]: attribute, comment: 'a comment' }) + .expect(400); + } + }); + + it('unhappy path - 400s when missing attributes for type alert', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(requestAttributes) + .expect(400); + } + }); + + it('unhappy path - 400s when adding excess attributes for type alert', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + for (const attribute of ['comment']) { + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + type: CommentType.alert, + [attribute]: attribute, + alertId: 'test-id', + index: 'test-index', + }) + .expect(400); + } + }); + + it('unhappy path - 400s when case is missing', async () => { + await supertest + .post(`${CASES_URL}/not-exists/comments`) + .set('kbn-xsrf', 'true') .send({ bad: 'comment', }) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 73d17b985216a..ac64818fe629e 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; -import { postCaseReq, postCommentReq } from '../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq } from '../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -49,7 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index 17814868fecc0..b119c71664f59 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; -import { postCaseReq, postCommentReq, findCasesResp } from '../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq, findCasesResp } from '../../../common/lib/mock'; import { deleteCases, deleteComments, deleteCasesUserActions } from '../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -98,13 +98,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body } = await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index 80cf2c8199807..3cf0d6892377e 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../alerting_api_integration/common/lib'; import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../plugins/case/common/constants'; -import { postCaseReq, defaultUser, postCommentReq } from '../../../common/lib/mock'; +import { postCaseReq, defaultUser, postCommentUserReq } from '../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, @@ -130,7 +130,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body } = await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index 92ef544ee9b37..6949052df4703 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { defaultUser, postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { CommentType } from '../../../../../../plugins/case/common/api'; +import { defaultUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, @@ -251,7 +252,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const { body } = await supertest @@ -264,7 +265,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(body[1].action_field).to.eql(['comment']); expect(body[1].action).to.eql('create'); expect(body[1].old_value).to.eql(null); - expect(body[1].new_value).to.eql(postCommentReq.comment); + expect(body[1].new_value).to.eql(JSON.stringify(postCommentUserReq)); }); it(`on update comment, user action: 'update' should be called with actionFields: ['comments']`, async () => { @@ -277,7 +278,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq) + .send(postCommentUserReq) .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -285,6 +286,7 @@ export default ({ getService }: FtrProviderContext): void => { id: patchedCase.comments[0].id, version: patchedCase.comments[0].version, comment: newComment, + type: CommentType.user, }); const { body } = await supertest @@ -296,8 +298,13 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.length).to.eql(3); expect(body[2].action_field).to.eql(['comment']); expect(body[2].action).to.eql('update'); - expect(body[2].old_value).to.eql(postCommentReq.comment); - expect(body[2].new_value).to.eql(newComment); + expect(body[2].old_value).to.eql(JSON.stringify(postCommentUserReq)); + expect(body[2].new_value).to.eql( + JSON.stringify({ + comment: newComment, + type: CommentType.user, + }) + ); }); it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index 7a351d09b5b9f..9a45dd541bb56 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { CommentType } from '../../../../../plugins/case/common/api'; import { postCaseReq, postCaseResp, @@ -616,9 +618,9 @@ export default ({ getService }: FtrProviderContext): void => { createdActionId = createdAction.id; const params = { - subAction: 'update', + subAction: 'addComment', subActionParams: { - comment: { comment: 'a comment', type: 'user' }, + comment: { comment: 'a comment', type: CommentType.user }, }, }; @@ -632,12 +634,12 @@ export default ({ getService }: FtrProviderContext): void => { status: 'error', actionId: createdActionId, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', retry: false, }); }); - it('should respond with a 400 Bad Request when adding a comment to a case without comment', async () => { + it('should respond with a 400 Bad Request when missing attributes of type user', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -650,7 +652,7 @@ export default ({ getService }: FtrProviderContext): void => { createdActionId = createdAction.id; const params = { - subAction: 'update', + subAction: 'addComment', subActionParams: { caseId: '123', }, @@ -666,12 +668,143 @@ export default ({ getService }: FtrProviderContext): void => { status: 'error', actionId: createdActionId, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: expected at least one defined value but got [undefined]', retry: false, }); }); - it('should respond with a 400 Bad Request when adding a comment to a case without comment type', async () => { + it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const comment = { alertId: 'test-id', index: 'test-index', type: CommentType.alert }; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment, + }, + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, comment); + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { ...params.subActionParams, comment: requestAttributes }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: expected value of type [string] but got [undefined]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding excess attributes for type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + for (const attribute of ['alertId', 'index']) { + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { + ...params.subActionParams, + comment: { ...params.subActionParams.comment, [attribute]: attribute }, + }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing\n - [subActionParams.comment.1.type]: expected value to equal [alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment: { alertId: 'test-id', index: 'test-index', type: CommentType.alert }, + }, + }; + + for (const attribute of ['comment']) { + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { + ...params.subActionParams, + comment: { ...params.subActionParams.comment, [attribute]: attribute }, + }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: definition for this key is missing`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding a comment to a case without type', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -706,7 +839,60 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should add a comment', async () => { + it('should add a comment of type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalComment: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + + it('should add a comment of type alert', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -729,7 +915,7 @@ export default ({ getService }: FtrProviderContext): void => { subAction: 'addComment', subActionParams: { caseId: caseRes.body.id, - comment: { comment: 'a comment', type: 'user' }, + comment: { alertId: 'test-id', index: 'test-index', type: CommentType.alert }, }, }; diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index d2262c684dc6d..a1e7f9a7fa89e 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -10,6 +10,9 @@ import { CasesFindResponse, CommentResponse, ConnectorTypes, + CommentRequestUserType, + CommentRequestAlertType, + CommentType, } from '../../../../plugins/case/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; export const postCaseReq: CasePostRequest = { @@ -24,9 +27,15 @@ export const postCaseReq: CasePostRequest = { }, }; -export const postCommentReq: { comment: string; type: string } = { +export const postCommentUserReq: CommentRequestUserType = { comment: 'This is a cool comment', - type: 'user', + type: CommentType.user, +}; + +export const postCommentAlertReq: CommentRequestAlertType = { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, }; export const postCaseResp = ( diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index c682c1f1f4640..b653d46905503 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { PrePackagedRulesAndTimelinesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,6 +14,7 @@ import { deleteAllAlerts, deleteAllTimelines, deleteSignalsIndex, + installPrePackagedRules, waitFor, } from '../../utils'; @@ -45,18 +47,27 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllTimelines(es); }); - it('should contain rules_installed, rules_updated, timelines_installed, and timelines_updated', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(Object.keys(body)).to.eql([ + it('should create the prepackaged rules and return a count greater than zero, rules_updated to be zero, and contain the correct keys', async () => { + let responseBody: unknown; + await waitFor(async () => { + const { body, status } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send(); + if (status === 200) { + responseBody = body; + } + return status === 200; + }, DETECTION_ENGINE_PREPACKAGED_URL); + + const prepackagedRules = responseBody as PrePackagedRulesAndTimelinesSchema; + expect(prepackagedRules.rules_installed).to.be.greaterThan(0); + expect(prepackagedRules.rules_updated).to.eql(0); + expect(Object.keys(prepackagedRules)).to.eql([ 'rules_installed', 'rules_updated', 'timelines_installed', @@ -64,52 +75,8 @@ export default ({ getService }: FtrProviderContext): void => { ]); }); - it('should create the prepackaged rules and return a count greater than zero', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.rules_installed).to.be.greaterThan(0); - }); - - it('should create the prepackaged timelines and return a count greater than zero', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.timelines_installed).to.be.greaterThan(0); - }); - - it('should create the prepackaged rules that the rules_updated is of size zero', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.rules_updated).to.eql(0); - }); - - it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.timelines_updated).to.eql(0); - }); - - it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { - await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + it('should be possible to call the API twice and the second time the number of rules installed should be zero as well as timeline', async () => { + await installPrePackagedRules(supertest); // NOTE: I call the GET call until eventually it becomes consistent and that the number of rules to install are zero. // This is to reduce flakiness where it can for a short period of time try to install the same rule twice. @@ -119,39 +86,23 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); return body.rules_not_installed === 0; - }); - - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.rules_installed).to.eql(0); - }); - - it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { - await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + }, `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`); + let responseBody: unknown; await waitFor(async () => { - const { body } = await supertest - .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + const { body, status } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') - .expect(200); - return body.timelines_not_installed === 0; - }); - - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.timelines_installed).to.eql(0); + .send(); + if (status === 200) { + responseBody = body; + } + return status === 200; + }, DETECTION_ENGINE_PREPACKAGED_URL); + + const prepackagedRules = responseBody as PrePackagedRulesAndTimelinesSchema; + expect(prepackagedRules.rules_installed).to.eql(0); + expect(prepackagedRules.timelines_installed).to.eql(0); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 53a8f1f4ca5c0..a8a5f2abd072b 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -25,7 +25,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('create_rules', () => { describe('validation errors', () => { @@ -51,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should create a single rule with a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts index 6c3b1c45e202e..73be4154db1eb 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('create_rules_bulk', () => { describe('validation errors', () => { @@ -54,7 +53,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should create a single rule with a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts index 7104e16f438c6..786e953843210 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('delete_rules', () => { describe('deleting rules', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should delete a single rule with a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts index 35b31d2ccfefa..66aa43e8a3817 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('delete_rules_bulk', () => { describe('deleting rules bulk using DELETE', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should delete a single rule with a rule_id', async () => { @@ -146,7 +145,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should delete a single rule with a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts index 2610796bdc384..4f76a0544a152 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts @@ -22,7 +22,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('export_rules', () => { describe('exporting rules', () => { @@ -32,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should set the response content types to be expected', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts index f496d035d8e60..2f06a84c7223b 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('find_rules', () => { beforeEach(async () => { @@ -32,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should return an empty find body correctly if no rules are loaded', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts index 9c20d58c5f4e5..fe80402b60731 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts @@ -30,7 +30,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllRulesStatuses(es); }); @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { - const resBody = await createRule(supertest, getSimpleRule()); + const resBody = await createRule(supertest, getSimpleRule('rule-1', true)); await waitForRuleSuccess(supertest, resBody.id); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts index 1bbfce42d2baa..c72b2e50b39fc 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts @@ -32,7 +32,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllTimelines(es); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts index c6294cfe6ec28..f5774e09bb5e9 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('import_rules', () => { describe('importing rules without an index', () => { @@ -39,7 +38,7 @@ export default ({ getService }: FtrProviderContext): void => { .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) .send(); return body.status_code === 404; - }); + }, `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`); // Try to fetch the rule which should still be a 404 (not found) const { body } = await supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`).send(); @@ -86,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should set the response content types to be expected', async () => { @@ -129,7 +128,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') .expect(200); const { body } = await supertest @@ -138,7 +137,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1')); + expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1', false)); }); it('should fail validation when importing a rule with malformed "from" params on the rules', async () => { @@ -330,7 +329,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') .expect(200); const simpleRule = getSimpleRule('rule-1'); @@ -422,17 +421,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2'], true), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') .expect(200); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach( - 'file', - getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3'], true), - 'rules.ndjson' - ) + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') .expect(200); const { body: bodyOfRule1 } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts index 556217877968b..f70720cc752b2 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts @@ -29,7 +29,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllTimelines(es); }); @@ -72,7 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); return body.timelines_not_installed === 0; - }); + }, `${TIMELINE_PREPACKAGED_URL}/_status`); const { body } = await supertest .put(TIMELINE_PREPACKAGED_URL) diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts index a84d9845085e0..f8a25b0081ef9 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts @@ -18,19 +18,19 @@ import { deleteSignalsIndex, setSignalStatus, getSignalStatusEmptyResponse, - getSimpleRule, getQuerySignalIds, deleteAllAlerts, createRule, waitForSignalsToBePresent, - getAllSignals, + getSignalsByIds, + waitForRuleSuccess, + getRuleForSignalTesting, } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); describe('open_close_signals', () => { describe('validation checks', () => { @@ -66,29 +66,31 @@ export default ({ getService }: FtrProviderContext) => { describe('tests with auditbeat data', () => { beforeEach(async () => { - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await createSignalsIndex(supertest); await esArchiver.load('auditbeat/hosts'); }); afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await esArchiver.unload('auditbeat/hosts'); }); it('should be able to execute and get 10 signals', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 10); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).equal(10); }); it('should be have set the signals in an open state initially', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); const everySignalOpen = signalsOpen.hits.hits.every( ({ _source: { @@ -100,10 +102,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able to get a count of 10 closed signals when closing 10', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 10); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); // set all of the signals to the state of closed. There is no reason to use a waitUntil here @@ -126,10 +129,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able close 10 signals immediately and they all should be closed', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); // set all of the signals to the state of closed. There is no reason to use a waitUntil here diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts index 36a9649d875ca..28ea2e1ff8803 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('patch_rules', () => { describe('patch rules', () => { @@ -33,7 +32,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should patch a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts index 69330a2bf682a..e32771d0d917c 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('patch_rules_bulk', () => { describe('patch rules bulk', () => { @@ -33,7 +32,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should patch a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts index cfccb7436ea20..1697554441c16 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('read_rules', () => { describe('reading rules', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should be able to read a single rule using rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts index 2f5a043881eeb..d8e9c650c8116 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts @@ -25,7 +25,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('update_rules', () => { describe('update rules', () => { @@ -35,7 +34,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should update a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts index 22aa40b0721a4..c5b65039aa116 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('update_rules_bulk', () => { describe('update rules bulk', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should update a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index d473863e7d028..bbd85e353e095 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('add_actions', () => { describe('adding actions', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should be able to create a new webhook action and attach it to a rule', async () => { @@ -60,7 +59,7 @@ export default ({ getService }: FtrProviderContext) => { .send(getWebHookAction()) .expect(200); - const rule = await createRule(supertest, getRuleWithWebHookAction(hookAction.id)); + const rule = await createRule(supertest, getRuleWithWebHookAction(hookAction.id, true)); await waitForRuleSuccess(supertest, rule.id); // expected result for status should be 'succeeded' @@ -82,7 +81,7 @@ export default ({ getService }: FtrProviderContext) => { // create a rule with the action attached and a meta field const ruleWithAction: CreateRulesSchema = { - ...getRuleWithWebHookAction(hookAction.id), + ...getRuleWithWebHookAction(hookAction.id, true), meta: {}, }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts index c889e152759a8..b653d46905503 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { PrePackagedRulesAndTimelinesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,6 +14,7 @@ import { deleteAllAlerts, deleteAllTimelines, deleteSignalsIndex, + installPrePackagedRules, waitFor, } from '../../utils'; @@ -45,18 +47,27 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllTimelines(es); }); - it('should contain two output keys of rules_installed and rules_updated', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(Object.keys(body)).to.eql([ + it('should create the prepackaged rules and return a count greater than zero, rules_updated to be zero, and contain the correct keys', async () => { + let responseBody: unknown; + await waitFor(async () => { + const { body, status } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send(); + if (status === 200) { + responseBody = body; + } + return status === 200; + }, DETECTION_ENGINE_PREPACKAGED_URL); + + const prepackagedRules = responseBody as PrePackagedRulesAndTimelinesSchema; + expect(prepackagedRules.rules_installed).to.be.greaterThan(0); + expect(prepackagedRules.rules_updated).to.eql(0); + expect(Object.keys(prepackagedRules)).to.eql([ 'rules_installed', 'rules_updated', 'timelines_installed', @@ -64,74 +75,34 @@ export default ({ getService }: FtrProviderContext): void => { ]); }); - it('should create the prepackaged rules and return a count greater than zero', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.rules_installed).to.be.greaterThan(0); - }); - - it('should create the prepackaged rules that the rules_updated is of size zero', async () => { - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.rules_updated).to.eql(0); - }); - - it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { - await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + it('should be possible to call the API twice and the second time the number of rules installed should be zero as well as timeline', async () => { + await installPrePackagedRules(supertest); // NOTE: I call the GET call until eventually it becomes consistent and that the number of rules to install are zero. - // This is to reduce flakiness where it can for a short period of time try to install the same rule the same rule twice. + // This is to reduce flakiness where it can for a short period of time try to install the same rule twice. await waitFor(async () => { const { body } = await supertest .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) .set('kbn-xsrf', 'true') .expect(200); return body.rules_not_installed === 0; - }); - - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.rules_installed).to.eql(0); - }); - - it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { - await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + }, `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`); + let responseBody: unknown; await waitFor(async () => { - const { body } = await supertest - .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + const { body, status } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') - .expect(200); - return body.timelines_not_installed === 0; - }); - - const { body } = await supertest - .put(DETECTION_ENGINE_PREPACKAGED_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.timelines_installed).to.eql(0); + .send(); + if (status === 200) { + responseBody = body; + } + return status === 200; + }, DETECTION_ENGINE_PREPACKAGED_URL); + + const prepackagedRules = responseBody as PrePackagedRulesAndTimelinesSchema; + expect(prepackagedRules.rules_installed).to.eql(0); + expect(prepackagedRules.timelines_installed).to.eql(0); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index 651a7601ca95a..7e4a6ad86cda5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -32,7 +32,7 @@ import { createExceptionList, createExceptionListItem, waitForSignalsToBePresent, - getAllSignals, + getSignalsByIds, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -49,7 +49,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllExceptions(es); }); @@ -101,6 +101,7 @@ export default ({ getService }: FtrProviderContext) => { const ruleWithException: CreateRulesSchema = { ...getSimpleRule(), + enabled: true, exceptions_list: [ { id, @@ -117,6 +118,7 @@ export default ({ getService }: FtrProviderContext) => { const expected: Partial<RulesSchema> = { ...getSimpleRuleOutput(), + enabled: true, exceptions_list: [ { id, @@ -397,7 +399,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllExceptions(es); await esArchiver.unload('auditbeat/hosts'); }); @@ -441,9 +443,10 @@ export default ({ getService }: FtrProviderContext) => { }, ], }; - await createRule(supertest, ruleWithException); - await waitForSignalsToBePresent(supertest, 10); - const signalsOpen = await getAllSignals(supertest); + const { id: createdId } = await createRule(supertest, ruleWithException); + await waitForRuleSuccess(supertest, createdId); + await waitForSignalsToBePresent(supertest, 10, [createdId]); + const signalsOpen = await getSignalsByIds(supertest, [createdId]); expect(signalsOpen.hits.hits.length).equal(10); }); @@ -488,7 +491,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, ruleWithException); await waitForRuleSuccess(supertest, rule.id); - const signalsOpen = await getAllSignals(supertest); + const signalsOpen = await getSignalsByIds(supertest, [rule.id]); expect(signalsOpen.hits.hits.length).equal(0); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index a18faf8543042..0da12ebba055a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -25,12 +25,12 @@ import { getSimpleMlRule, getSimpleMlRuleOutput, waitForRuleSuccess, + getRuleForSignalTesting, } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('create_rules', () => { describe('validation errors', () => { @@ -56,7 +56,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should create a single rule with a rule_id', async () => { @@ -90,7 +90,7 @@ export default ({ getService }: FtrProviderContext) => { this pops up again elsewhere. */ it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getRuleForSignalTesting(['auditbeat-*']); const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') @@ -105,8 +105,6 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: [body.id] }) .expect(200); - const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(getSimpleRuleOutput()); expect(statusBody[body.id].current_status.status).to.eql('succeeded'); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index 58790dbfb759c..7ea47312a5030 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -15,6 +15,7 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, + getRuleForSignalTesting, getSimpleRule, getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, @@ -27,7 +28,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('create_rules_bulk', () => { describe('validation errors', () => { @@ -58,7 +58,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should create a single rule with a rule_id', async () => { @@ -92,7 +92,7 @@ export default ({ getService }: FtrProviderContext): void => { this pops up again elsewhere. */ it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getRuleForSignalTesting(['auditbeat-*']); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) .set('kbn-xsrf', 'true') @@ -107,8 +107,6 @@ export default ({ getService }: FtrProviderContext): void => { .send({ ids: [body[0].id] }) .expect(200); - const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(getSimpleRuleOutput()); expect(statusBody[body[0].id].current_status.status).to.eql('succeeded'); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 36cd8480998c5..21cfab3db6d6a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -17,7 +17,7 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, - getAllSignals, + getSignalsByIds, removeServerGeneratedProperties, waitForRuleSuccess, waitForSignalsToBePresent, @@ -30,7 +30,6 @@ import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); /** * Specific api integration tests for threat matching rule type @@ -59,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should create a single rule with a rule_id', async () => { @@ -69,7 +68,10 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const ruleResponse = await createRule(supertest, getCreateThreatMatchRulesSchemaMock()); + const ruleResponse = await createRule( + supertest, + getCreateThreatMatchRulesSchemaMock('rule-1', true) + ); await waitForRuleSuccess(supertest, ruleResponse.id); const { body: statusBody } = await supertest @@ -79,21 +81,21 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); const bodyToCompare = removeServerGeneratedProperties(ruleResponse); - expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock(true)); expect(statusBody[ruleResponse.id].current_status.status).to.eql('succeeded'); }); }); describe('tests with auditbeat data', () => { beforeEach(async () => { - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await createSignalsIndex(supertest); await esArchiver.load('auditbeat/hosts'); }); afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await esArchiver.unload('auditbeat/hosts'); }); @@ -125,9 +127,10 @@ export default ({ getService }: FtrProviderContext) => { threat_filters: [], }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 10); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).equal(10); }); @@ -161,7 +164,7 @@ export default ({ getService }: FtrProviderContext) => { const ruleResponse = await createRule(supertest, rule); await waitForRuleSuccess(supertest, ruleResponse.id); - const signalsOpen = await getAllSignals(supertest); + const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); @@ -199,7 +202,7 @@ export default ({ getService }: FtrProviderContext) => { const ruleResponse = await createRule(supertest, rule); await waitForRuleSuccess(supertest, ruleResponse.id); - const signalsOpen = await getAllSignals(supertest); + const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); @@ -237,7 +240,7 @@ export default ({ getService }: FtrProviderContext) => { const ruleResponse = await createRule(supertest, rule); await waitForRuleSuccess(supertest, ruleResponse.id); - const signalsOpen = await getAllSignals(supertest); + const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts index 7104e16f438c6..786e953843210 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('delete_rules', () => { describe('deleting rules', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should delete a single rule with a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts index 35b31d2ccfefa..66aa43e8a3817 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('delete_rules_bulk', () => { describe('deleting rules bulk using DELETE', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should delete a single rule with a rule_id', async () => { @@ -146,7 +145,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should delete a single rule with a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md new file mode 100644 index 0000000000000..d6beb912d7007 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md @@ -0,0 +1,21 @@ +These are tests for rule exception lists where we test each data type +* date +* double +* float +* integer +* ip +* keyword +* long +* text + +Against the operator types of: +* "is" +* "is not" +* "is one of" +* "is not one of" +* "exists" +* "does not exist" +* "is in list" +* "is not in list" + +If you add a test here, ensure you add it to the ./index.ts" file as well \ No newline at end of file diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts new file mode 100644 index 0000000000000..09cc470defa08 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts @@ -0,0 +1,611 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type date', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/date'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/date'); + }); + + describe('"is" operator', () => { + it('should find all the dates from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ]); + }); + + it('should filter 1 single date if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-01T05:08:53.000Z', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([ + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ]); + }); + + it('should filter 2 dates if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-01T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-02T05:08:53.000Z', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-03T05:08:53.000Z', '2020-10-04T05:08:53.000Z']); + }); + + it('should filter 3 dates if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-01T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-02T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-03T05:08:53.000Z', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-04T05:08:53.000Z']); + }); + + it('should filter 4 dates if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-01T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-02T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-03T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'included', + type: 'match', + value: '2020-10-04T05:08:53.000Z', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'excluded', + type: 'match', + value: '2021-10-01T05:08:53.000Z', // date is not in data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'excluded', + type: 'match', + value: '2020-10-01T05:08:53.000Z', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-01T05:08:53.000Z']); + }); + + it('will return 0 results if we exclude two dates', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'excluded', + type: 'match', + value: '2020-10-01T05:08:53.000Z', + }, + ], + [ + { + field: 'date', + operator: 'excluded', + type: 'match', + value: '2020-10-02T05:08:53.000Z', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single date if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match_any', + value: ['2020-10-01T05:08:53.000Z'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([ + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ]); + }); + + it('should filter 2 dates if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match_any', + value: ['2020-10-01T05:08:53.000Z', '2020-10-02T05:08:53.000Z'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-03T05:08:53.000Z', '2020-10-04T05:08:53.000Z']); + }); + + it('should filter 3 dates if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match_any', + value: [ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + ], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-04T05:08:53.000Z']); + }); + + it('should filter 4 dates if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'match_any', + value: [ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'excluded', + type: 'match_any', + value: ['2021-10-01T05:08:53.000Z', '2022-10-01T05:08:53.000Z'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'excluded', + type: 'match_any', + value: ['2020-10-01T05:08:53.000Z', '2020-10-04T05:08:53.000Z'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-01T05:08:53.000Z', '2020-10-04T05:08:53.000Z']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against date', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against date', async () => { + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ]); + }); + }); + + describe('"is in list" operator', () => { + it('will return 3 results if we have a list that includes 1 date', async () => { + await importFile(supertest, 'date', ['2020-10-01T05:08:53.000Z'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + list: { + id: 'list_items.txt', + type: 'date', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([ + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ]); + }); + + it('will return 2 results if we have a list that includes 2 dates', async () => { + await importFile( + supertest, + 'date', + ['2020-10-01T05:08:53.000Z', '2020-10-03T05:08:53.000Z'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + list: { + id: 'list_items.txt', + type: 'date', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-02T05:08:53.000Z', '2020-10-04T05:08:53.000Z']); + }); + + it('will return 0 results if we have a list that includes all dates', async () => { + await importFile( + supertest, + 'date', + [ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + list: { + id: 'list_items.txt', + type: 'date', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not in list" operator', () => { + it('will return 1 result if we have a list that excludes 1 date', async () => { + await importFile(supertest, 'date', ['2020-10-01T05:08:53.000Z'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + list: { + id: 'list_items.txt', + type: 'date', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-01T05:08:53.000Z']); + }); + + it('will return 2 results if we have a list that excludes 2 dates', async () => { + await importFile( + supertest, + 'date', + ['2020-10-01T05:08:53.000Z', '2020-10-03T05:08:53.000Z'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + list: { + id: 'list_items.txt', + type: 'date', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql(['2020-10-01T05:08:53.000Z', '2020-10-03T05:08:53.000Z']); + }); + + it('will return 4 results if we have a list that excludes all dates', async () => { + await importFile( + supertest, + 'date', + [ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['date']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'date', + list: { + id: 'list_items.txt', + type: 'date', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); + expect(hits).to.eql([ + '2020-10-01T05:08:53.000Z', + '2020-10-02T05:08:53.000Z', + '2020-10-03T05:08:53.000Z', + '2020-10-04T05:08:53.000Z', + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts new file mode 100644 index 0000000000000..e29487880de6b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts @@ -0,0 +1,744 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type double', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/double'); + await esArchiver.load('rule_exceptions/double_as_string'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/double'); + await esArchiver.unload('rule_exceptions/double_as_string'); + }); + + describe('"is" operator', () => { + it('should find all the double from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + + it('should filter 1 single double if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('should filter 2 double if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.2', '1.3']); + }); + + it('should filter 3 double if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.1', + }, + ], + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.3']); + }); + + it('should filter 4 double if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.1', + }, + ], + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.2', + }, + ], + [ + { + field: 'double', + operator: 'included', + type: 'match', + value: '1.3', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'excluded', + type: 'match', + value: '1.0', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0']); + }); + + it('will return 0 results if we exclude two double', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'excluded', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'double', + operator: 'excluded', + type: 'match', + value: '1.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single double if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match_any', + value: ['1.0'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('should filter 2 double if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match_any', + value: ['1.0', '1.1'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.2', '1.3']); + }); + + it('should filter 3 double if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match_any', + value: ['1.0', '1.1', '1.2'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.3']); + }); + + it('should filter 4 double if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'match_any', + value: ['1.0', '1.1', '1.2', '1.3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'excluded', + type: 'match_any', + value: ['1.0', '1.3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.3']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against double', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against double', async () => { + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + }); + + describe('"is in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a double against an index that has the doubles stored as real doubles. + describe.skip('working against double values in the data set', () => { + it('will return 3 results if we have a list that includes 1 double', async () => { + await importFile(supertest, 'double', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('will return 2 results if we have a list that includes 2 double', async () => { + await importFile(supertest, 'double', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.1', '1.3']); + }); + + it('will return 0 results if we have a list that includes all double', async () => { + await importFile(supertest, 'double', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 3 results if we have a list that includes 1 double', async () => { + await importFile(supertest, 'double', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('will return 2 results if we have a list that includes 2 double', async () => { + await importFile(supertest, 'double', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.1', '1.3']); + }); + + it('will return 0 results if we have a list that includes all double', async () => { + await importFile(supertest, 'double', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql([]); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 1 result if we have a list which contains the double range of 1.0-1.2', async () => { + await importFile(supertest, 'double_range', ['1.0-1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['1.3']); + }); + }); + }); + + describe('"is not in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a double against an index that has the doubles stored as real doubles. + describe.skip('working against double values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 double', async () => { + await importFile(supertest, 'double', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0']); + }); + + it('will return 2 results if we have a list that excludes 2 double', async () => { + await importFile(supertest, 'double', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.2']); + }); + + it('will return 4 results if we have a list that excludes all double', async () => { + await importFile(supertest, 'double', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 double', async () => { + await importFile(supertest, 'double', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0']); + }); + + it('will return 2 results if we have a list that excludes 2 double', async () => { + await importFile(supertest, 'double', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.2']); + }); + + it('will return 4 results if we have a list that excludes all double', async () => { + await importFile(supertest, 'double', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'double', + list: { + id: 'list_items.txt', + type: 'double', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 3 results if we have a list which contains the double range of 1.0-1.2', async () => { + await importFile(supertest, 'double_range', ['1.0-1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['double_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2']); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts new file mode 100644 index 0000000000000..d68f0f6a69277 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts @@ -0,0 +1,744 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type float', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/float'); + await esArchiver.load('rule_exceptions/float_as_string'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/float'); + await esArchiver.unload('rule_exceptions/float_as_string'); + }); + + describe('"is" operator', () => { + it('should find all the float from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + + it('should filter 1 single float if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('should filter 2 float if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.2', '1.3']); + }); + + it('should filter 3 float if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.1', + }, + ], + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.3']); + }); + + it('should filter 4 float if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.1', + }, + ], + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.2', + }, + ], + [ + { + field: 'float', + operator: 'included', + type: 'match', + value: '1.3', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'excluded', + type: 'match', + value: '1.0', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0']); + }); + + it('will return 0 results if we exclude two float', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'excluded', + type: 'match', + value: '1.0', + }, + ], + [ + { + field: 'float', + operator: 'excluded', + type: 'match', + value: '1.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single float if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match_any', + value: ['1.0'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('should filter 2 float if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match_any', + value: ['1.0', '1.1'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.2', '1.3']); + }); + + it('should filter 3 float if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match_any', + value: ['1.0', '1.1', '1.2'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.3']); + }); + + it('should filter 4 float if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'match_any', + value: ['1.0', '1.1', '1.2', '1.3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'excluded', + type: 'match_any', + value: ['1.0', '1.3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.3']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against float', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against float', async () => { + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + }); + + describe('"is in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a float against an index that has the floats stored as real floats. + describe.skip('working against float values in the data set', () => { + it('will return 3 results if we have a list that includes 1 float', async () => { + await importFile(supertest, 'float', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('will return 2 results if we have a list that includes 2 float', async () => { + await importFile(supertest, 'float', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.1', '1.3']); + }); + + it('will return 0 results if we have a list that includes all float', async () => { + await importFile(supertest, 'float', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 3 results if we have a list that includes 1 float', async () => { + await importFile(supertest, 'float', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.1', '1.2', '1.3']); + }); + + it('will return 2 results if we have a list that includes 2 float', async () => { + await importFile(supertest, 'float', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.1', '1.3']); + }); + + it('will return 0 results if we have a list that includes all float', async () => { + await importFile(supertest, 'float', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql([]); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 1 result if we have a list which contains the float range of 1.0-1.2', async () => { + await importFile(supertest, 'float_range', ['1.0-1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['1.3']); + }); + }); + }); + + describe('"is not in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a float against an index that has the floats stored as real floats. + describe.skip('working against float values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 float', async () => { + await importFile(supertest, 'float', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0']); + }); + + it('will return 2 results if we have a list that excludes 2 float', async () => { + await importFile(supertest, 'float', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.2']); + }); + + it('will return 4 results if we have a list that excludes all float', async () => { + await importFile(supertest, 'float', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 float', async () => { + await importFile(supertest, 'float', ['1.0'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0']); + }); + + it('will return 2 results if we have a list that excludes 2 float', async () => { + await importFile(supertest, 'float', ['1.0', '1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.2']); + }); + + it('will return 4 results if we have a list that excludes all float', async () => { + await importFile(supertest, 'float', ['1.0', '1.1', '1.2', '1.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'float', + list: { + id: 'list_items.txt', + type: 'float', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 3 results if we have a list which contains the float range of 1.0-1.2', async () => { + await importFile(supertest, 'float_range', ['1.0-1.2'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['float_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['1.0', '1.1', '1.2']); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts new file mode 100644 index 0000000000000..d2aca34e27399 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + this.tags('ciGroup1'); + + loadTestFile(require.resolve('./date')); + loadTestFile(require.resolve('./double')); + loadTestFile(require.resolve('./float')); + loadTestFile(require.resolve('./integer')); + loadTestFile(require.resolve('./ip')); + loadTestFile(require.resolve('./keyword')); + loadTestFile(require.resolve('./long')); + loadTestFile(require.resolve('./text')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts new file mode 100644 index 0000000000000..9b38f0f7cbb42 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts @@ -0,0 +1,744 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type integer', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/integer'); + await esArchiver.load('rule_exceptions/integer_as_string'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/integer'); + await esArchiver.unload('rule_exceptions/integer_as_string'); + }); + + describe('"is" operator', () => { + it('should find all the integer from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + + it('should filter 1 single integer if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('should filter 2 integer if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['3', '4']); + }); + + it('should filter 3 integer if all 3 are as exceptions', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '2', + }, + ], + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '3', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['4']); + }); + + it('should filter 4 integer if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '2', + }, + ], + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '3', + }, + ], + [ + { + field: 'integer', + operator: 'included', + type: 'match', + value: '4', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'excluded', + type: 'match', + value: '1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1']); + }); + + it('will return 0 results if we exclude two integer', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'excluded', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'integer', + operator: 'excluded', + type: 'match', + value: '2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single integer if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match_any', + value: ['1'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('should filter 2 integer if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match_any', + value: ['1', '2'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['3', '4']); + }); + + it('should filter 3 integer if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match_any', + value: ['1', '2', '3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['4']); + }); + + it('should filter 4 integer if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'match_any', + value: ['1', '2', '3', '4'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'excluded', + type: 'match_any', + value: ['1', '4'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '4']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against integer', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against integer', async () => { + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + }); + + describe('"is in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a integer against an index that has the integers stored as real integers. + describe.skip('working against integer values in the data set', () => { + it('will return 3 results if we have a list that includes 1 integer', async () => { + await importFile(supertest, 'integer', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('will return 2 results if we have a list that includes 2 integer', async () => { + await importFile(supertest, 'integer', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['2', '4']); + }); + + it('will return 0 results if we have a list that includes all integer', async () => { + await importFile(supertest, 'integer', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 3 results if we have a list that includes 1 integer', async () => { + await importFile(supertest, 'integer', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('will return 2 results if we have a list that includes 2 integer', async () => { + await importFile(supertest, 'integer', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['2', '4']); + }); + + it('will return 0 results if we have a list that includes all integer', async () => { + await importFile(supertest, 'integer', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql([]); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 1 result if we have a list which contains the integer range of 1-3', async () => { + await importFile(supertest, 'integer_range', ['1-3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['4']); + }); + }); + }); + + describe('"is not in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a integer against an index that has the integers stored as real integers. + describe.skip('working against integer values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 integer', async () => { + await importFile(supertest, 'integer', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1']); + }); + + it('will return 2 results if we have a list that excludes 2 integer', async () => { + await importFile(supertest, 'integer', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '3']); + }); + + it('will return 4 results if we have a list that excludes all integer', async () => { + await importFile(supertest, 'integer', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 integer', async () => { + await importFile(supertest, 'integer', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1']); + }); + + it('will return 2 results if we have a list that excludes 2 integer', async () => { + await importFile(supertest, 'integer', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '3']); + }); + + it('will return 4 results if we have a list that excludes all integer', async () => { + await importFile(supertest, 'integer', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'integer', + list: { + id: 'list_items.txt', + type: 'integer', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 3 results if we have a list which contains the integer range of 1-3', async () => { + await importFile(supertest, 'integer_range', ['1-3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['integer_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['1', '2', '3']); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts new file mode 100644 index 0000000000000..c3537efc12de7 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts @@ -0,0 +1,622 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type ip', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/ip'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/ip'); + }); + + describe('"is" operator', () => { + it('should find all the ips from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']); + }); + + it('should filter 1 single ip if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.2', '127.0.0.3', '127.0.0.4']); + }); + + it('should filter 2 ips if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.3', '127.0.0.4']); + }); + + it('should filter 3 ips if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.2', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.3', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.4']); + }); + + it('should filter 4 ips if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.2', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.3', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.4', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + + it('should filter a CIDR range of 127.0.0.1/30', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1/30', // CIDR IP Range is 127.0.0.0 - 127.0.0.3 + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.4']); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '192.168.0.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1']); + }); + + it('will return 0 results if we exclude two ips', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single ip if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.2', '127.0.0.3', '127.0.0.4']); + }); + + it('should filter 2 ips if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.2'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.3', '127.0.0.4']); + }); + + it('should filter 3 ips if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.2', '127.0.0.3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.4']); + }); + + it('should filter 4 ips if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match_any', + value: ['192.168.0.1', '192.168.0.2'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.4'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.4']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against ip', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against ip', async () => { + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']); + }); + }); + + describe('"is in list" operator', () => { + it('will return 3 results if we have a list that includes 1 ip', async () => { + await importFile(supertest, 'ip', ['127.0.0.1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.2', '127.0.0.3', '127.0.0.4']); + }); + + it('will return 2 results if we have a list that includes 2 ips', async () => { + await importFile(supertest, 'ip', ['127.0.0.1', '127.0.0.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.2', '127.0.0.4']); + }); + + it('will return 0 results if we have a list that includes all ips', async () => { + await importFile( + supertest, + 'ip', + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 1 result if we have a list which contains the CIDR range of 127.0.0.1/30', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1/30'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.4']); + }); + }); + + describe('"is not in list" operator', () => { + it('will return 1 result if we have a list that excludes 1 ip', async () => { + await importFile(supertest, 'ip', ['127.0.0.1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1']); + }); + + it('will return 2 results if we have a list that excludes 2 ips', async () => { + await importFile(supertest, 'ip', ['127.0.0.1', '127.0.0.3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.3']); + }); + + it('will return 4 results if we have a list that excludes all ips', async () => { + await importFile( + supertest, + 'ip', + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 3 results if we have a list which contains the CIDR range of 127.0.0.1/30', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1/30'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.2', '127.0.0.3']); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts new file mode 100644 index 0000000000000..0c227c9acc38c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts @@ -0,0 +1,555 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type keyword', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/keyword'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/keyword'); + }); + + describe('"is" operator', () => { + it('should find all the keyword from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + + it('should filter 1 single keyword if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('should filter 2 keyword if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word two', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word three']); + }); + + it('should filter 3 keyword if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word two', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word three', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four']); + }); + + it('should filter 4 keyword if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word two', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word three', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word four', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word one']); + }); + + it('will return 0 results if we exclude two keyword', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: 'word two', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single keyword if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word one'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('should filter 2 keyword if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word one', 'word two'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word three']); + }); + + it('should filter 3 keyword if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word one', 'word three', 'word two'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four']); + }); + + it('should filter 4 keyword if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word four', 'word one', 'word three', 'word two'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match_any', + value: ['word one', 'word four'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word one']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against keyword', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against keyword', async () => { + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + }); + + describe('"is in list" operator', () => { + it('will return 3 results if we have a list that includes 1 keyword', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('will return 2 results if we have a list that includes 2 keyword', async () => { + await importFile(supertest, 'keyword', ['word one', 'word three'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word two']); + }); + + it('will return 0 results if we have a list that includes all keyword', async () => { + await importFile( + supertest, + 'keyword', + ['word one', 'word two', 'word three', 'word four'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not in list" operator', () => { + it('will return 1 result if we have a list that excludes 1 keyword', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word one']); + }); + + it('will return 2 results if we have a list that excludes 2 keyword', async () => { + await importFile(supertest, 'keyword', ['word one', 'word three'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word one', 'word three']); + }); + + it('will return 4 results if we have a list that excludes all keyword', async () => { + await importFile( + supertest, + 'keyword', + ['word one', 'word two', 'word three', 'word four'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts new file mode 100644 index 0000000000000..5c110996c2198 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts @@ -0,0 +1,744 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type long', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/long'); + await esArchiver.load('rule_exceptions/long_as_string'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/long'); + await esArchiver.unload('rule_exceptions/long_as_string'); + }); + + describe('"is" operator', () => { + it('should find all the long from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + + it('should filter 1 single long if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('should filter 2 long if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['3', '4']); + }); + + it('should filter 3 long if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '2', + }, + ], + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '3', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['4']); + }); + + it('should filter 4 long if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '2', + }, + ], + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '3', + }, + ], + [ + { + field: 'long', + operator: 'included', + type: 'match', + value: '4', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'excluded', + type: 'match', + value: '1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1']); + }); + + it('will return 0 results if we exclude two long', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'excluded', + type: 'match', + value: '1', + }, + ], + [ + { + field: 'long', + operator: 'excluded', + type: 'match', + value: '2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single long if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match_any', + value: ['1'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('should filter 2 long if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match_any', + value: ['1', '2'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['3', '4']); + }); + + it('should filter 3 long if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match_any', + value: ['1', '2', '3'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['4']); + }); + + it('should filter 4 long if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'match_any', + value: ['1', '2', '3', '4'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'excluded', + type: 'match_any', + value: ['1', '4'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '4']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against long', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against long', async () => { + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + }); + + describe('"is in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a long against an index that has the longs stored as real longs. + describe.skip('working against long values in the data set', () => { + it('will return 3 results if we have a list that includes 1 long', async () => { + await importFile(supertest, 'long', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('will return 2 results if we have a list that includes 2 long', async () => { + await importFile(supertest, 'long', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['2', '4']); + }); + + it('will return 0 results if we have a list that includes all long', async () => { + await importFile(supertest, 'long', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 3 results if we have a list that includes 1 long', async () => { + await importFile(supertest, 'long', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['2', '3', '4']); + }); + + it('will return 2 results if we have a list that includes 2 long', async () => { + await importFile(supertest, 'long', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['2', '4']); + }); + + it('will return 0 results if we have a list that includes all long', async () => { + await importFile(supertest, 'long', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql([]); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 1 result if we have a list which contains the long range of 1-3', async () => { + await importFile(supertest, 'long_range', ['1-3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['4']); + }); + }); + }); + + describe('"is not in list" operator', () => { + // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent + // a long against an index that has the longs stored as real longs. + describe.skip('working against long values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 long', async () => { + await importFile(supertest, 'long', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1']); + }); + + it('will return 2 results if we have a list that excludes 2 long', async () => { + await importFile(supertest, 'long', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '3']); + }); + + it('will return 4 results if we have a list that excludes all long', async () => { + await importFile(supertest, 'long', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + }); + + describe('working against string values in the data set', () => { + it('will return 1 result if we have a list that excludes 1 long', async () => { + await importFile(supertest, 'long', ['1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1']); + }); + + it('will return 2 results if we have a list that excludes 2 long', async () => { + await importFile(supertest, 'long', ['1', '3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '3']); + }); + + it('will return 4 results if we have a list that excludes all long', async () => { + await importFile(supertest, 'long', ['1', '2', '3', '4'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'long', + list: { + id: 'list_items.txt', + type: 'long', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); + expect(hits).to.eql(['1', '2', '3', '4']); + }); + + // TODO: Fix this bug and then unskip this test + it.skip('will return 3 results if we have a list which contains the long range of 1-3', async () => { + await importFile(supertest, 'long_range', ['1-3'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['long_as_string']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(hits).to.eql(['1', '2', '3']); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts new file mode 100644 index 0000000000000..d2066b1023d3c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -0,0 +1,827 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, + importTextFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type text', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/text'); + await esArchiver.load('rule_exceptions/text_no_spaces'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/text'); + await esArchiver.unload('rule_exceptions/text_no_spaces'); + }); + + describe('"is" operator', () => { + it('should find all the text from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + + it('should filter 1 single text if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('should filter 2 text if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word two', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three']); + }); + + it('should filter 3 text if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word two', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word three', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four']); + }); + + it('should filter 4 text if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word two', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word three', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word four', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('should filter 1 single text using a single word', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('should filter all words using a common piece of text', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('should filter 1 single text with punctuation added', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'one.', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word one']); + }); + + it('will return 0 results if we exclude two text', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word two', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('should filter 1 single text using a single word', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word one']); + }); + + it('should filter all words using a common piece of text', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + + it('should filter 1 single text with punctuation added', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'one.', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word one']); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single text if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word one'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('should filter 2 text if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word one', 'word two'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three']); + }); + + it('should filter 3 text if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word one', 'word three', 'word two'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four']); + }); + + it('should filter 4 text if all are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word four', 'word one', 'word three', 'word two'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match_any', + value: ['word one', 'word four'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word one']); + }); + }); + + describe('"exists" operator', () => { + it('will return 0 results if matching against text', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 4 results if matching against text', async () => { + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + }); + + describe('"is in list" operator', () => { + describe('working against text values without spaces', () => { + it('will return 3 results if we have a list that includes 1 text', async () => { + await importFile(supertest, 'text', ['one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_no_spaces']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['four', 'three', 'two']); + }); + + it('will return 2 results if we have a list that includes 2 text', async () => { + await importFile(supertest, 'text', ['one', 'three'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_no_spaces']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['four', 'two']); + }); + + it('will return 0 results if we have a list that includes all text', async () => { + await importTextFile( + supertest, + 'text', + ['one', 'two', 'three', 'four'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['text_no_spaces']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + }); + + // TODO: Unskip these once this is fixed + describe.skip('working against text values with spaces', () => { + it('will return 3 results if we have a list that includes 1 text', async () => { + await importFile(supertest, 'text', ['one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('will return 2 results if we have a list that includes 2 text', async () => { + await importFile(supertest, 'text', ['one', 'three'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word two']); + }); + + it('will return 0 results if we have a list that includes all text', async () => { + await importTextFile( + supertest, + 'text', + ['one', 'two', 'three', 'four'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + }); + }); + + describe('"is not in list" operator', () => { + describe('working against text values without spaces', () => { + it('will return 1 result if we have a list that excludes 1 text', async () => { + await importTextFile(supertest, 'text', ['one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_no_spaces']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['one']); + }); + + it('will return 2 results if we have a list that excludes 2 text', async () => { + await importTextFile(supertest, 'text', ['one', 'three'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_no_spaces']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['one', 'three']); + }); + + it('will return 4 results if we have a list that excludes all text', async () => { + await importTextFile( + supertest, + 'text', + ['one', 'two', 'three', 'four'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['text_no_spaces']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['four', 'one', 'three', 'two']); + }); + }); + + // TODO: Unskip these once this is fixed + describe.skip('working against text values with spaces', () => { + it('will return 1 result if we have a list that excludes 1 text', async () => { + await importTextFile(supertest, 'text', ['one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word one']); + }); + + it('will return 2 results if we have a list that excludes 2 text', async () => { + await importTextFile(supertest, 'text', ['one', 'three'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word one', 'word three']); + }); + + it('will return 4 results if we have a list that excludes all text', async () => { + await importTextFile( + supertest, + 'text', + ['one', 'two', 'three', 'four'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts index 2610796bdc384..4f76a0544a152 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts @@ -22,7 +22,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('export_rules', () => { describe('exporting rules', () => { @@ -32,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should set the response content types to be expected', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts index f496d035d8e60..2f06a84c7223b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('find_rules', () => { beforeEach(async () => { @@ -32,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should return an empty find body correctly if no rules are loaded', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts index fac1fbaaf9675..8bb4c45d91bdd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts @@ -30,7 +30,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllRulesStatuses(es); }); @@ -64,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { this pops up again elsewhere. */ it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { - const resBody = await createRule(supertest, getSimpleRule()); + const resBody = await createRule(supertest, getSimpleRule('rule-1', true)); await waitForRuleSuccess(supertest, resBody.id); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index f76bdb4ebc718..0db3013503a33 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -17,9 +17,11 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, - getAllSignals, + getRuleForSignalTesting, + getSignalsByIds, getSignalsByRuleIds, getSimpleRule, + waitForRuleSuccess, waitForSignalsToBePresent, } from '../../utils'; @@ -33,17 +35,15 @@ export const ID = 'BhbXBmkBR346wHgn4PeZ'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); describe('Generating signals from source indexes', () => { beforeEach(async () => { - await deleteAllAlerts(es); await createSignalsIndex(supertest); }); afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); describe('Signals from audit beat are of the expected structure', () => { @@ -57,37 +57,37 @@ export default ({ getService }: FtrProviderContext) => { it('should have the specific audit record for _id or none of these tests below will pass', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['auditbeat-*']), query: `_id:${ID}`, }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).greaterThan(0); }); it('should have recorded the rule_id within the signal', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['auditbeat-*']), query: `_id:${ID}`, }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits[0]._source.signal.rule.rule_id).eql(getSimpleRule().rule_id); }); it('should query and get back expected signal structure using a basic KQL query', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['auditbeat-*']), query: `_id:${ID}`, }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); // remove rule to cut down on touch points for test changes when the rule format changes const { rule: removedRule, ...signalNoRule } = signalsOpen.hits.hits[0]._source.signal; expect(signalNoRule).eql({ @@ -126,25 +126,23 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure when it is a signal on a signal', async () => { - // create a 1 signal from 1 auditbeat record const rule: QueryCreateSchema = { - ...getSimpleRule(), - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['auditbeat-*']), query: `_id:${ID}`, }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); + const { id: createdId } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, createdId); + await waitForSignalsToBePresent(supertest, 1, [createdId]); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal const ruleForSignals: QueryCreateSchema = { - ...getSimpleRule(), + ...getRuleForSignalTesting([`${DEFAULT_SIGNALS_INDEX}*`]), rule_id: 'signal-on-signal', - index: [`${DEFAULT_SIGNALS_INDEX}*`], - from: '1900-01-01T00:00:00.000Z', - query: '*:*', }; - await createRule(supertest, ruleForSignals); - await waitForSignalsToBePresent(supertest, 2); + + const { id } = await createRule(supertest, ruleForSignals); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); // Get our single signal on top of a signal const signalsOpen = await getSignalsByRuleIds(supertest, ['signal-on-signal']); @@ -198,15 +196,15 @@ export default ({ getService }: FtrProviderContext) => { describe('EQL Rules', () => { it('generates signals from EQL sequences in the expected form', async () => { const rule: EqlCreateSchema = { - ...getSimpleRule(), - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['auditbeat-*']), rule_id: 'eql-rule', type: 'eql', language: 'eql', query: 'sequence by host.name [any where true] [any where true]', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); const signals = await getSignalsByRuleIds(supertest, ['eql-rule']); const signal = signals.hits.hits[0]._source.signal; @@ -250,15 +248,15 @@ export default ({ getService }: FtrProviderContext) => { it('generates building block signals from EQL sequences in the expected form', async () => { const rule: EqlCreateSchema = { - ...getSimpleRule(), - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['auditbeat-*']), rule_id: 'eql-rule', type: 'eql', language: 'eql', query: 'sequence by host.name [any where true] [any where true]', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByRuleIds(supertest, ['eql-rule']); const sequenceSignal = signalsOpen.hits.hits.find( (signal) => signal._source.signal.depth === 2 @@ -337,40 +335,39 @@ export default ({ getService }: FtrProviderContext) => { it('should have the specific audit record for _id or none of these tests below will pass', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_name_clash'], - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['signal_name_clash']), query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).greaterThan(0); }); it('should have recorded the rule_id within the signal', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_name_clash'], - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['signal_name_clash']), query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits[0]._source.signal.rule.rule_id).eql(getSimpleRule().rule_id); }); it('should query and get back expected signal structure using a basic KQL query', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_name_clash'], - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['signal_name_clash']), query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); // remove rule to cut down on touch points for test changes when the rule format changes const { rule: removedRule, ...signalNoRule } = signalsOpen.hits.hits[0]._source.signal; expect(signalNoRule).eql({ @@ -404,26 +401,22 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure when it is a signal on a signal', async () => { - // create a 1 signal from 1 auditbeat record const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_name_clash'], - from: '1900-01-01T00:00:00.000Z', - query: `_id:1`, + ...getRuleForSignalTesting(['signal_name_clash']), + query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal const ruleForSignals: QueryCreateSchema = { - ...getSimpleRule(), + ...getRuleForSignalTesting([`${DEFAULT_SIGNALS_INDEX}*`]), rule_id: 'signal-on-signal', - index: [`${DEFAULT_SIGNALS_INDEX}*`], - from: '1900-01-01T00:00:00.000Z', - query: '*:*', }; - await createRule(supertest, ruleForSignals); - await waitForSignalsToBePresent(supertest, 2); + const { id: createdId } = await createRule(supertest, ruleForSignals); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [createdId]); // Get our single signal on top of a signal const signalsOpen = await getSignalsByRuleIds(supertest, ['signal-on-signal']); @@ -479,7 +472,7 @@ export default ({ getService }: FtrProviderContext) => { * You should see the "signal" object/clash being copied to "original_signal" underneath * the signal object and no errors when they do have a clash. */ - describe('Signals generated from name clashes', () => { + describe('Signals generated from object clashes', () => { beforeEach(async () => { await esArchiver.load('signals/object_clash'); }); @@ -490,40 +483,37 @@ export default ({ getService }: FtrProviderContext) => { it('should have the specific audit record for _id or none of these tests below will pass', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_object_clash'], - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['signal_object_clash']), query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).greaterThan(0); }); it('should have recorded the rule_id within the signal', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_object_clash'], - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['signal_object_clash']), query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits[0]._source.signal.rule.rule_id).eql(getSimpleRule().rule_id); }); it('should query and get back expected signal structure using a basic KQL query', async () => { const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_object_clash'], - from: '1900-01-01T00:00:00.000Z', + ...getRuleForSignalTesting(['signal_object_clash']), query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); - const signalsOpen = await getAllSignals(supertest); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); // remove rule to cut down on touch points for test changes when the rule format changes const { rule: removedRule, ...signalNoRule } = signalsOpen.hits.hits[0]._source.signal; expect(signalNoRule).eql({ @@ -563,26 +553,22 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure when it is a signal on a signal', async () => { - // create a 1 signal from 1 auditbeat record const rule: QueryCreateSchema = { - ...getSimpleRule(), - index: ['signal_object_clash'], - from: '1900-01-01T00:00:00.000Z', - query: `_id:1`, + ...getRuleForSignalTesting(['signal_object_clash']), + query: '_id:1', }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 1); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal const ruleForSignals: QueryCreateSchema = { - ...getSimpleRule(), + ...getRuleForSignalTesting([`${DEFAULT_SIGNALS_INDEX}*`]), rule_id: 'signal-on-signal', - index: [`${DEFAULT_SIGNALS_INDEX}*`], - from: '1900-01-01T00:00:00.000Z', - query: '*:*', }; - await createRule(supertest, ruleForSignals); - await waitForSignalsToBePresent(supertest, 2); + const { id: createdId } = await createRule(supertest, ruleForSignals); + await waitForRuleSuccess(supertest, createdId); + await waitForSignalsToBePresent(supertest, 1, [createdId]); // Get our single signal on top of a signal const signalsOpen = await getSignalsByRuleIds(supertest, ['signal-on-signal']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts index 1bbfce42d2baa..c72b2e50b39fc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts @@ -32,7 +32,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await deleteAllTimelines(es); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index 664077d5a4fab..4ae953ead9df7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('es'); describe('import_rules', () => { describe('importing rules without an index', () => { @@ -39,7 +38,7 @@ export default ({ getService }: FtrProviderContext): void => { .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) .send(); return body.status_code === 404; - }); + }, `within should not create a rule if the index does not exist, ${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`); // Try to fetch the rule which should still be a 404 (not found) const { body } = await supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`).send(); @@ -86,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should set the response content types to be expected', async () => { @@ -129,7 +128,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') .expect(200); const { body } = await supertest @@ -138,7 +137,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1')); + expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1', false)); }); it('should be able to import two rules', async () => { @@ -243,7 +242,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') .expect(200); const simpleRule = getSimpleRule('rule-1'); @@ -335,17 +334,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2'], true), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') .expect(200); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach( - 'file', - getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3'], true), - 'rules.ndjson' - ) + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') .expect(200); const { body: bodyOfRule1 } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 962ae53b1241f..97d5b079fd206 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -19,6 +19,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./create_exceptions')); loadTestFile(require.resolve('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); + loadTestFile(require.resolve('./exception_operators_data_types/index')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); loadTestFile(require.resolve('./find_statuses')); 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 bbc3943b75955..87e3b145ed6fd 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 @@ -18,12 +18,13 @@ import { deleteSignalsIndex, setSignalStatus, getSignalStatusEmptyResponse, - getSimpleRule, getQuerySignalIds, deleteAllAlerts, createRule, waitForSignalsToBePresent, - getAllSignals, + getSignalsByIds, + waitForRuleSuccess, + getRuleForSignalTesting, } from '../../utils'; import { createUserAndRole } from '../roles_users_utils'; import { ROLES } from '../../../../plugins/security_solution/common/test'; @@ -32,7 +33,6 @@ import { ROLES } from '../../../../plugins/security_solution/common/test'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const securityService = getService('security'); @@ -69,29 +69,31 @@ export default ({ getService }: FtrProviderContext) => { describe('tests with auditbeat data', () => { beforeEach(async () => { - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await createSignalsIndex(supertest); await esArchiver.load('auditbeat/hosts'); }); afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); await esArchiver.unload('auditbeat/hosts'); }); it('should be able to execute and get 10 signals', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 10); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).equal(10); }); it('should be have set the signals in an open state initially', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); const everySignalOpen = signalsOpen.hits.hits.every( ({ _source: { @@ -103,10 +105,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able to get a count of 10 closed signals when closing 10', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest, 10); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 10, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); // set all of the signals to the state of closed. There is no reason to use a waitUntil here @@ -129,10 +132,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able close signals immediately and they all should be closed', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest); - const signalsOpen = await getAllSignals(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); // set all of the signals to the state of closed. There is no reason to use a waitUntil here @@ -163,11 +167,12 @@ export default ({ getService }: FtrProviderContext) => { }); it('should NOT be able to close signals with t1 analyst user', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); await createUserAndRole(securityService, ROLES.t1_analyst); - const signalsOpen = await getAllSignals(supertest); + const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); // Try to set all of the signals to the state of closed. @@ -200,12 +205,13 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able to close signals with soc_manager user', async () => { - const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; - await createRule(supertest, rule); - await waitForSignalsToBePresent(supertest); + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); const userAndRole = ROLES.soc_manager; await createUserAndRole(securityService, userAndRole); - const signalsOpen = await getAllSignals(supertest); + const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); // Try to set all of the signals to the state of closed. diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts index dbe66741e06c7..4de8abefe16fc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -25,7 +25,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('patch_rules', () => { describe('patch rules', () => { @@ -35,7 +34,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should patch a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts index 69330a2bf682a..e32771d0d917c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -23,7 +23,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('patch_rules_bulk', () => { describe('patch rules bulk', () => { @@ -33,7 +32,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should patch a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts index cfccb7436ea20..1697554441c16 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('read_rules', () => { describe('reading rules', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should be able to read a single rule using rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 23a8776b14631..59dbe97557157 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -27,7 +27,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('update_rules', () => { describe('update rules', () => { @@ -37,7 +36,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should update a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index 22aa40b0721a4..c5b65039aa116 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -24,7 +24,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es = getService('es'); describe('update_rules_bulk', () => { describe('update rules bulk', () => { @@ -34,7 +33,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest); - await deleteAllAlerts(es); + await deleteAllAlerts(supertest); }); it('should update a single rule property of name using a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index f458fe118dcf7..06d33da8f1f55 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -9,6 +9,8 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; +import { NonEmptyEntriesArray } from '../../plugins/lists/common/schemas'; +import { getCreateExceptionListDetectionSchemaMock } from '../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { CreateRulesSchema, UpdateRulesSchema, @@ -35,6 +37,7 @@ import { DETECTION_ENGINE_RULES_URL, INTERNAL_RULE_ID_KEY, } from '../../plugins/security_solution/common/constants'; +import { getCreateExceptionListItemMinimalSchemaMockWithoutId } from '../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; /** * This will remove server generated properties such as date times, etc... @@ -76,9 +79,9 @@ export const removeServerGeneratedPropertiesIncludingRuleId = ( /** * This is a typical simple rule for testing that is easy for most basic testing * @param ruleId - * @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import + * @param enabled Enables the rule on creation or not. Defaulted to true. */ -export const getSimpleRule = (ruleId = 'rule-1', enabled = true): QueryCreateSchema => ({ +export const getSimpleRule = (ruleId = 'rule-1', enabled = false): QueryCreateSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', enabled, @@ -90,13 +93,39 @@ export const getSimpleRule = (ruleId = 'rule-1', enabled = true): QueryCreateSch query: 'user.name: root or user.name: admin', }); +/** + * This is a typical signal testing rule that is easy for most basic testing of output of signals. + * It starts out in an enabled true state. The from is set very far back to test the basics of signal + * creation and testing by getting all the signals at once. + * @param ruleId The optional ruleId which is rule-1 by default. + * @param enabled Enables the rule on creation or not. Defaulted to true. + */ +export const getRuleForSignalTesting = ( + index: string[], + ruleId = 'rule-1', + enabled = true +): QueryCreateSchema => ({ + name: 'Signal Testing Query', + description: 'Tests a simple query', + enabled, + risk_score: 1, + rule_id: ruleId, + severity: 'high', + index, + type: 'query', + query: '*:*', + from: '1900-01-01T00:00:00.000Z', +}); + /** * This is a typical simple rule for testing that is easy for most basic testing - * @param ruleId + * @param ruleId The rule id + * @param enabled Set to tru to enable it, by default it is off */ -export const getSimpleRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ +export const getSimpleRuleUpdate = (ruleId = 'rule-1', enabled = false): UpdateRulesSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', + enabled, risk_score: 1, rule_id: ruleId, severity: 'high', @@ -107,11 +136,13 @@ export const getSimpleRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ /** * This is a representative ML rule payload as expected by the server - * @param ruleId + * @param ruleId The rule id + * @param enabled Set to tru to enable it, by default it is off */ -export const getSimpleMlRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ +export const getSimpleMlRule = (ruleId = 'rule-1', enabled = false): CreateRulesSchema => ({ name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', + enabled, anomaly_threshold: 44, risk_score: 1, rule_id: ruleId, @@ -120,9 +151,15 @@ export const getSimpleMlRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ type: 'machine_learning', }); -export const getSimpleMlRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ +/** + * This is a representative ML rule payload as expected by the server for an update + * @param ruleId The rule id + * @param enabled Set to tru to enable it, by default it is off + */ +export const getSimpleMlRuleUpdate = (ruleId = 'rule-1', enabled = false): UpdateRulesSchema => ({ name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', + enabled, anomaly_threshold: 44, risk_score: 1, rule_id: ruleId, @@ -160,6 +197,19 @@ export const getQuerySignalsRuleId = (ruleIds: string[]) => ({ }, }); +/** + * Given an array of ids for a test this will get the signals + * created from that rule's regular id. + * @param ruleIds The rule_id to search for signals + */ +export const getQuerySignalsId = (ids: string[]) => ({ + query: { + terms: { + 'signal.rule.id': ids, + }, + }, +}); + export const setSignalStatus = ({ signalIds, status, @@ -216,12 +266,12 @@ export const binaryToString = (res: any, callback: any): void => { * This is the typical output of a simple rule that Kibana will output with all the defaults * except for the server generated properties. Useful for testing end to end tests. */ -export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial<RulesSchema> => ({ +export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial<RulesSchema> => ({ actions: [], author: [], created_by: 'elastic', description: 'Simple Rule Query', - enabled: true, + enabled, false_positives: [], from: 'now-6m', immutable: false, @@ -274,21 +324,38 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial<RulesSchema> = }; /** - * Remove all alerts from the .kibana index - * This will retry 20 times before giving up and hopefully still not interfere with other tests - * @param es The ElasticSearch handle + * Removes all rules by looping over any found and removing them from REST. + * @param supertest The supertest agent. */ -export const deleteAllAlerts = async (es: Client): Promise<void> => { - return countDownES(async () => { - return es.deleteByQuery({ - index: '.kibana', - q: 'type:alert', - wait_for_completion: true, - refresh: true, - conflicts: 'proceed', - body: {}, - }); - }, 'deleteAllAlerts'); +export const deleteAllAlerts = async ( + supertest: SuperTest<supertestAsPromised.Test> +): Promise<void> => { + await countDownTest( + async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999`) + .set('kbn-xsrf', 'true') + .send(); + + const ids = body.data.map((rule: FullResponseSchema) => ({ + id: rule.id, + })); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send(ids) + .set('kbn-xsrf', 'true'); + + const { body: finalCheck } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send(); + return finalCheck.data.length === 0; + }, + 'deleteAllAlerts', + 50, + 1000 + ); }; export const downgradeImmutableRule = async (es: Client, ruleId: string): Promise<void> => { @@ -331,7 +398,7 @@ export const deleteAllTimelines = async (es: Client): Promise<void> => { * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promise<void> => { +export const deleteAllRulesStatuses = async (es: Client): Promise<void> => { return countDownES(async () => { return es.deleteByQuery({ index: '.kibana', @@ -585,8 +652,8 @@ export const getWebHookAction = () => ({ name: 'Some connector', }); -export const getRuleWithWebHookAction = (id: string): CreateRulesSchema => ({ - ...getSimpleRule(), +export const getRuleWithWebHookAction = (id: string, enabled = false): CreateRulesSchema => ({ + ...getSimpleRule('rule-1', enabled), throttle: 'rule', actions: [ { @@ -618,7 +685,8 @@ export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial< // Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor export const waitFor = async ( functionToTest: () => Promise<boolean>, - maxTimeout: number = 5000, + functionName: string, + maxTimeout: number = 10000, timeoutWait: number = 10 ): Promise<void> => { await new Promise(async (resolve, reject) => { @@ -636,7 +704,9 @@ export const waitFor = async ( if (found) { resolve(); } else { - reject(new Error('timed out waiting for function condition to be true')); + reject( + new Error(`timed out waiting for function condition to be true within ${functionName}`) + ); } }); }; @@ -807,7 +877,7 @@ export const waitForRuleSuccess = async ( .send({ ids: [id] }) .expect(200); return body[id]?.current_status?.status === 'succeeded'; - }); + }, 'waitForRuleSuccess'); }; /** @@ -818,51 +888,77 @@ export const waitForRuleSuccess = async ( */ export const waitForSignalsToBePresent = async ( supertest: SuperTest<supertestAsPromised.Test>, - numberOfSignals = 1 + numberOfSignals = 1, + signalIds: string[] ): Promise<void> => { await waitFor(async () => { - const { - body: signalsOpen, - }: { body: SearchResponse<{ signal: Signal }> } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQueryAllSignals()) - .expect(200); + const signalsOpen = await getSignalsByIds(supertest, signalIds); return signalsOpen.hits.hits.length >= numberOfSignals; - }); + }, 'waitForSignalsToBePresent'); }; /** - * Returns all signals both closed and opened + * Returns all signals both closed and opened by ruleId * @param supertest Deps */ -export const getAllSignals = async ( - supertest: SuperTest<supertestAsPromised.Test> +export const getSignalsByRuleIds = async ( + supertest: SuperTest<supertestAsPromised.Test>, + ruleIds: string[] ): Promise< SearchResponse<{ signal: Signal; + [x: string]: unknown; }> > => { const { body: signalsOpen }: { body: SearchResponse<{ signal: Signal }> } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') - .send(getQueryAllSignals()) + .send(getQuerySignalsRuleId(ruleIds)) .expect(200); return signalsOpen; }; -export const getSignalsByRuleIds = async ( +/** + * Given an array of rule ids this will return only signals based on that rule id both + * open and closed + * @param supertest agent + * @param ids Array of the rule ids + */ +export const getSignalsByIds = async ( supertest: SuperTest<supertestAsPromised.Test>, - ruleIds: string[] + ids: string[] ): Promise< SearchResponse<{ signal: Signal; + [x: string]: unknown; }> > => { const { body: signalsOpen }: { body: SearchResponse<{ signal: Signal }> } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') - .send(getQuerySignalsRuleId(ruleIds)) + .send(getQuerySignalsId(ids)) + .expect(200); + return signalsOpen; +}; + +/** + * Given a single rule id this will return only signals based on that rule id. + * @param supertest agent + * @param ids Rule id + */ +export const getSignalsById = async ( + supertest: SuperTest<supertestAsPromised.Test>, + id: string +): Promise< + SearchResponse<{ + signal: Signal; + [x: string]: unknown; + }> +> => { + const { body: signalsOpen }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalsId([id])) .expect(200); return signalsOpen; }; @@ -870,5 +966,77 @@ export const getSignalsByRuleIds = async ( export const installPrePackagedRules = async ( supertest: SuperTest<supertestAsPromised.Test> ): Promise<void> => { - await supertest.put(DETECTION_ENGINE_PREPACKAGED_URL).set('kbn-xsrf', 'true').send().expect(200); + await countDownTest(async () => { + const { status } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send(); + return status === 200; + }, 'installPrePackagedRules'); +}; + +/** + * Convenience testing function where you can pass in just the entries and you will + * get a rule created with the entries added to an exception list and exception list item + * all auto-created at once. + * @param supertest super test agent + * @param rule The rule to create and attach an exception list to + * @param entries The entries to create the rule and exception list from + */ +export const createRuleWithExceptionEntries = async ( + supertest: SuperTest<supertestAsPromised.Test>, + rule: QueryCreateSchema, + entries: NonEmptyEntriesArray[] +): Promise<FullResponseSchema> => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, list_id, namespace_type, type } = await createExceptionList( + supertest, + getCreateExceptionListDetectionSchemaMock() + ); + + await Promise.all( + entries.map((entry) => { + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMockWithoutId(), + entries: entry, + }; + return createExceptionListItem(supertest, exceptionListItem); + }) + ); + + // To reduce the odds of in-determinism and/or bugs we ensure we have + // the same length of entries before continuing. + await waitFor(async () => { + const { body } = await supertest.get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }` + ); + return body.data.length === entries.length; + }, `within createRuleWithExceptionEntries ${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${getCreateExceptionListDetectionSchemaMock().list_id}`); + + // create the rule but don't run it immediately as running it immediately can cause + // the rule to sometimes not filter correctly the first time with an exception list + // or other timing issues. Then afterwards wait for the rule to have succeeded before + // returning. + const ruleWithException: QueryCreateSchema = { + ...rule, + enabled: false, + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + const ruleResponse = await createRule(supertest, ruleWithException); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleResponse.rule_id, enabled: true }) + .expect(200); + + return ruleResponse; }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/actions.ts b/x-pack/test/fleet_api_integration/apis/agents/actions.ts index 01f69328388db..d97ac6f7daa6e 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/actions.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/actions.ts @@ -36,6 +36,39 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.item.data).to.eql({ data: 'action_data' }); }); + it('should return a 200 if this a valid SETTINGS action request', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + type: 'SETTINGS', + data: { log_level: 'debug' }, + }, + }) + .expect(200); + + expect(apiResponse.item.type).to.eql('SETTINGS'); + expect(apiResponse.item.data).to.eql({ log_level: 'debug' }); + }); + + it('should return a 400 if this a invalid SETTINGS action request', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + type: 'SETTINGS', + data: { log_level: 'thisnotavalidloglevel' }, + }, + }) + .expect(400); + + expect(apiResponse.message).to.match( + /\[request body.action\.[0-9]*\.data\.log_level]: types that failed validation/ + ); + }); + it('should return a 400 when request does not have type information', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/agents/agent1/actions`) @@ -43,12 +76,11 @@ export default function (providerContext: FtrProviderContext) { .send({ action: { data: { data: 'action_data' }, - sent_at: '2020-03-18T19:45:02.620Z', }, }) .expect(400); - expect(apiResponse.message).to.eql( - '[request body.action.type]: expected at least one defined value but got [undefined]' + expect(apiResponse.message).to.match( + /\[request body.action\.[0-9]*\.type]: expected at least one defined value but got \[undefined]/ ); }); @@ -60,7 +92,6 @@ export default function (providerContext: FtrProviderContext) { action: { type: 'POLICY_CHANGE', data: { data: 'action_data' }, - sent_at: '2020-03-18T19:45:02.620Z', }, }) .expect(404); diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index c6de3a7f2b9dc..53982affa128c 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -4,16 +4,73 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; +import fs from 'fs'; +import path from 'path'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const log = getService('log'); const supertest = getService('supertest'); const dockerServers = getService('dockerServers'); const server = dockerServers.get('registry'); + const testPkgKey = 'apache-0.1.4'; + + const uninstallPackage = async (pkg: string) => { + await supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + const installPackage = async (pkg: string) => { + await supertest + .post(`/api/fleet/epm/packages/${pkg}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + const testPkgArchiveZip = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.zip' + ); + describe('EPM - get', () => { + it('returns package info from the registry if it was installed from the registry', async function () { + if (server.enabled) { + // this will install through the registry by default + await installPackage(testPkgKey); + const res = await supertest.get(`/api/fleet/epm/packages/${testPkgKey}`).expect(200); + const packageInfo = res.body.response; + // the uploaded version will have this description + expect(packageInfo.description).to.not.equal('Apache Uploaded Test Integration'); + // download property should exist + expect(packageInfo.download).to.not.equal(undefined); + await uninstallPackage(testPkgKey); + } else { + warnAndSkipTest(this, log); + } + }); + it('returns correct package info if it was installed by upload', async function () { + if (server.enabled) { + const buf = fs.readFileSync(testPkgArchiveZip); + await supertest + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/zip') + .send(buf) + .expect(200); + + const res = await supertest.get(`/api/fleet/epm/packages/${testPkgKey}`).expect(200); + const packageInfo = res.body.response; + // the uploaded version will have this description + expect(packageInfo.description).to.equal('Apache Uploaded Test Integration'); + // download property should not exist on uploaded packages + expect(packageInfo.download).to.equal(undefined); + await uninstallPackage(testPkgKey); + } else { + warnAndSkipTest(this, log); + } + }); it('returns a 500 for a package key without a proper name', async function () { if (server.enabled) { await supertest.get('/api/fleet/epm/packages/-0.1.0').expect(500); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index a5f1aa8003f04..885386b092108 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -80,7 +80,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/zip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(18); + expect(res.body.response.length).to.be(23); }); it('should throw an error if the archive is zip but content type is gzip', async function () { diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz index 9cc4009d35c31..b1f2ac6797fb3 100644 Binary files a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz differ diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip index 410b00ecde2be..2095ed0dba345 100644 Binary files a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip differ diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts index 12de29c4fde10..d44a373f43040 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.preserveCrossAppState(); }); - it('should create dashboard to URL drilldown and use it to navigate to discover', async () => { + it.skip('should create dashboard to URL drilldown and use it to navigate to discover', async () => { await PageObjects.dashboard.gotoDashboardEditMode( dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME ); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts index 288804750277e..768bfb3a69fdf 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts @@ -23,7 +23,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); - describe('Explore underlying data - panel action', function () { + // FLAKY: https://github.com/elastic/kibana/issues/84011 + // FLAKY: https://github.com/elastic/kibana/issues/84012 + describe.skip('Explore underlying data - panel action', function () { before( 'change default index pattern to verify action navigates to correct index pattern', async () => { diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index 10754d20118e9..d612a3776d211 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'dashboard', 'maps']); + const PageObjects = getPageObjects(['common', 'dashboard', 'discover', 'maps']); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); @@ -48,16 +48,11 @@ export default function ({ getPageObjects, getService }) { }); describe('panel actions', () => { - before(async () => { + beforeEach(async () => { await loadDashboardAndOpenTooltip(); }); - it('should display more actions button when tooltip is locked', async () => { - const exists = await testSubjects.exists('mapTooltipMoreActionsButton'); - expect(exists).to.be(true); - }); - - it('should trigger drilldown action when clicked', async () => { + it('should trigger dashboard drilldown action when clicked', async () => { await testSubjects.click('mapTooltipMoreActionsButton'); await testSubjects.click('mapFilterActionButton__drilldown1'); @@ -69,6 +64,16 @@ export default function ({ getPageObjects, getService }) { const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); expect(hasJoinFilter).to.be(true); }); + + it('should trigger url drilldown action when clicked', async () => { + await testSubjects.click('mapTooltipMoreActionsButton'); + await testSubjects.click('mapFilterActionButton__urlDrilldownToDiscover'); + + // Assert on discover with filter from action + await PageObjects.discover.waitForDiscoverAppOnScreen(); + const hasFilter = await filterBar.hasFilter('name', 'charlie'); + expect(hasFilter).to.be(true); + }); }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 1557d2b4ec2fb..c759f22d0396c 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -88,7 +88,7 @@ export default function ({ getService }: FtrProviderContext) { } }); - describe('with no data loaded', function () { + describe('with data loaded', function () { const adJobId = 'fq_single_permission'; const dfaJobId = 'iph_outlier_permission'; const calendarId = 'calendar_permission'; diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 79e8c14cc3982..71b4a85d63f08 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1113,7 +1113,7 @@ "title" : "dash for tooltip filter action test", "hits" : 0, "description" : "Zoomed in so entire screen is covered by filter so click to open tooltip can not miss.", - "panelsJSON" : "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"669a3521-1215-4228-9ced-77e2edf5ad17\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"drilldown1\",\"config\":{\"dashboardId\":\"19906970-2e40-11e9-85cb-6965aae20f13\",\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_0\"}]", + "panelsJSON" : "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"669a3521-1215-4228-9ced-77e2edf5ad17\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"drilldown1\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}},{\"eventId\":\"b9c20d96-03ce-4dcc-8823-e3503311172e\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"name\":\"urlDrilldownToDiscover\",\"config\":{\"url\":{\"template\":\"{{kibanaUrl}}/app/discover#/?_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'561253e0-f731-11e8-8487-11b9dd924f96',key:{{event.key}},negate:!f,params:(query:{{event.value}}),type:phrase),query:(match_phrase:({{event.key}}:{{event.value}})))),index:'561253e0-f731-11e8-8487-11b9dd924f96',interval:auto,query:(language:kuery,query:''),sort:!())\"},\"openInNewTab\":false},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_0\"}]", "optionsJSON" : "{\"useMargins\":true,\"hidePanelTitles\":false}", "version" : 1, "timeRestore" : true, @@ -1129,6 +1129,11 @@ }, "type" : "dashboard", "references" : [ + { + "name" : "drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:669a3521-1215-4228-9ced-77e2edf5ad17:dashboardId", + "type" : "dashboard", + "id" : "19906970-2e40-11e9-85cb-6965aae20f13" + }, { "name" : "panel_0", "type" : "map", @@ -1136,9 +1141,9 @@ } ], "migrationVersion" : { - "dashboard" : "7.3.0" + "dashboard" : "7.11.0" }, - "updated_at" : "2020-08-26T14:32:27.854Z" + "updated_at" : "2020-11-19T15:12:25.703Z" } } } diff --git a/x-pack/test/functional/es_archives/ml/module_auditbeat/data.json.gz b/x-pack/test/functional/es_archives/ml/module_auditbeat/data.json.gz new file mode 100644 index 0000000000000..6a9b639397759 Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/module_auditbeat/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/module_auditbeat/mappings.json b/x-pack/test/functional/es_archives/ml/module_auditbeat/mappings.json new file mode 100644 index 0000000000000..1b7188b1410d8 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/module_auditbeat/mappings.json @@ -0,0 +1,4653 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_module_auditbeat", + "mappings": { + "_meta": { + "beat": "auditbeat", + "version": "7.8.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "dns.answers": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "dns.answers.*" + } + }, + { + "log.syslog": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "log.syslog.*" + } + }, + { + "network.inner": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "network.inner.*" + } + }, + { + "observer.egress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.egress.*" + } + }, + { + "observer.ingress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.ingress.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "auditd": { + "properties": { + "data": { + "properties": { + "a0": { + "ignore_above": 1024, + "type": "keyword" + }, + "a1": { + "ignore_above": 1024, + "type": "keyword" + }, + "a2": { + "ignore_above": 1024, + "type": "keyword" + }, + "a3": { + "ignore_above": 1024, + "type": "keyword" + }, + "a[0-3]": { + "ignore_above": 1024, + "type": "keyword" + }, + "acct": { + "ignore_above": 1024, + "type": "keyword" + }, + "acl": { + "ignore_above": 1024, + "type": "keyword" + }, + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "added": { + "ignore_above": 1024, + "type": "keyword" + }, + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "apparmor": { + "ignore_above": 1024, + "type": "keyword" + }, + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "argc": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_limit": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_wait_time": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_failure": { + "ignore_above": 1024, + "type": "keyword" + }, + "banners": { + "ignore_above": 1024, + "type": "keyword" + }, + "bool": { + "ignore_above": 1024, + "type": "keyword" + }, + "bus": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "capability": { + "ignore_above": 1024, + "type": "keyword" + }, + "cgroup": { + "ignore_above": 1024, + "type": "keyword" + }, + "changed": { + "ignore_above": 1024, + "type": "keyword" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "cmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "compat": { + "ignore_above": 1024, + "type": "keyword" + }, + "daddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "default-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "dmac": { + "ignore_above": 1024, + "type": "keyword" + }, + "dport": { + "ignore_above": 1024, + "type": "keyword" + }, + "enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "entries": { + "ignore_above": 1024, + "type": "keyword" + }, + "exit": { + "ignore_above": 1024, + "type": "keyword" + }, + "fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "fd": { + "ignore_above": 1024, + "type": "keyword" + }, + "fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "feature": { + "ignore_above": 1024, + "type": "keyword" + }, + "fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "file": { + "ignore_above": 1024, + "type": "keyword" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "format": { + "ignore_above": 1024, + "type": "keyword" + }, + "fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "grantors": { + "ignore_above": 1024, + "type": "keyword" + }, + "grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "hook": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "icmp_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "igid": { + "ignore_above": 1024, + "type": "keyword" + }, + "img-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "inif": { + "ignore_above": 1024, + "type": "keyword" + }, + "ino": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "invalid_context": { + "ignore_above": 1024, + "type": "keyword" + }, + "ioctlcmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipx-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "items": { + "ignore_above": 1024, + "type": "keyword" + }, + "iuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "ksize": { + "ignore_above": 1024, + "type": "keyword" + }, + "laddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "len": { + "ignore_above": 1024, + "type": "keyword" + }, + "list": { + "ignore_above": 1024, + "type": "keyword" + }, + "lport": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "macproto": { + "ignore_above": 1024, + "type": "keyword" + }, + "maj": { + "ignore_above": 1024, + "type": "keyword" + }, + "major": { + "ignore_above": 1024, + "type": "keyword" + }, + "minor": { + "ignore_above": 1024, + "type": "keyword" + }, + "model": { + "ignore_above": 1024, + "type": "keyword" + }, + "msg": { + "ignore_above": 1024, + "type": "keyword" + }, + "nargs": { + "ignore_above": 1024, + "type": "keyword" + }, + "net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ocomm": { + "ignore_above": 1024, + "type": "keyword" + }, + "oflag": { + "ignore_above": 1024, + "type": "keyword" + }, + "old": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-auid": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pa": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_val": { + "ignore_above": 1024, + "type": "keyword" + }, + "op": { + "ignore_above": 1024, + "type": "keyword" + }, + "opid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oses": { + "ignore_above": 1024, + "type": "keyword" + }, + "outif": { + "ignore_above": 1024, + "type": "keyword" + }, + "pa": { + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "per": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm_mask": { + "ignore_above": 1024, + "type": "keyword" + }, + "permissive": { + "ignore_above": 1024, + "type": "keyword" + }, + "pfs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "printer": { + "ignore_above": 1024, + "type": "keyword" + }, + "prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "proto": { + "ignore_above": 1024, + "type": "keyword" + }, + "qbytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "range": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "removed": { + "ignore_above": 1024, + "type": "keyword" + }, + "res": { + "ignore_above": 1024, + "type": "keyword" + }, + "resrc": { + "ignore_above": 1024, + "type": "keyword" + }, + "rport": { + "ignore_above": 1024, + "type": "keyword" + }, + "sauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "scontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "selected-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperm": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperms": { + "ignore_above": 1024, + "type": "keyword" + }, + "seqno": { + "ignore_above": 1024, + "type": "keyword" + }, + "seresult": { + "ignore_above": 1024, + "type": "keyword" + }, + "ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "sig": { + "ignore_above": 1024, + "type": "keyword" + }, + "sigev_signo": { + "ignore_above": 1024, + "type": "keyword" + }, + "smac": { + "ignore_above": 1024, + "type": "keyword" + }, + "socket": { + "properties": { + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "ignore_above": 1024, + "type": "keyword" + }, + "saddr": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "spid": { + "ignore_above": 1024, + "type": "keyword" + }, + "sport": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "subj": { + "ignore_above": 1024, + "type": "keyword" + }, + "success": { + "ignore_above": 1024, + "type": "keyword" + }, + "syscall": { + "ignore_above": 1024, + "type": "keyword" + }, + "table": { + "ignore_above": 1024, + "type": "keyword" + }, + "tclass": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + }, + "tty": { + "ignore_above": 1024, + "type": "keyword" + }, + "unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "uri": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "val": { + "ignore_above": 1024, + "type": "keyword" + }, + "ver": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "watch": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "paths": { + "properties": { + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "dev": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "item": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "nametype": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_role": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_user": { + "ignore_above": 1024, + "type": "keyword" + }, + "objtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "ogid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ouid": { + "ignore_above": 1024, + "type": "keyword" + }, + "rdev": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "summary": { + "properties": { + "actor": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "how": { + "ignore_above": 1024, + "type": "keyword" + }, + "object": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "fields": { + "raw": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "selinux": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "setgid": { + "type": "boolean" + }, + "setuid": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geoip": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "blake2b_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "xxh64": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "blake2b_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "xxh64": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "system": { + "properties": { + "audit": { + "properties": { + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "boottime": { + "type": "date" + }, + "containerized": { + "type": "boolean" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timezone": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "offset": { + "properties": { + "sec": { + "type": "long" + } + } + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "package": { + "properties": { + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "installtime": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "summary": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "properties": { + "last_changed": { + "type": "date" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "shell": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "user_information": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "audit": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "effective": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "filesystem": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name_map": { + "type": "object" + }, + "saved": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "selinux": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "5000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/ml/module_heartbeat/data.json.gz b/x-pack/test/functional/es_archives/ml/module_heartbeat/data.json.gz new file mode 100644 index 0000000000000..ba0b78aab3aa4 Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/module_heartbeat/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/module_heartbeat/mappings.json b/x-pack/test/functional/es_archives/ml/module_heartbeat/mappings.json new file mode 100644 index 0000000000000..e97531c6febf1 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/module_heartbeat/mappings.json @@ -0,0 +1,3390 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_module_heartbeat", + "mappings": { + "_meta": { + "beat": "heartbeat", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "dns.answers": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "dns.answers.*" + } + }, + { + "log.syslog": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "log.syslog.*" + } + }, + { + "network.inner": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "network.inner.*" + } + }, + { + "observer.egress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.egress.*" + } + }, + { + "observer.ingress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.ingress.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "redirects": { + "ignore_above": 1024, + "type": "keyword" + }, + "status_code": { + "type": "long" + } + } + }, + "rtt": { + "properties": { + "content": { + "properties": { + "us": { + "type": "long" + } + } + }, + "response_header": { + "properties": { + "us": { + "type": "long" + } + } + }, + "total": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate_body": { + "properties": { + "us": { + "type": "long" + } + } + }, + "write_request": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "icmp": { + "properties": { + "requests": { + "type": "long" + }, + "rtt": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "monitor": { + "properties": { + "check_group": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "timespan": { + "type": "date_range" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolve": { + "properties": { + "ip": { + "type": "ip" + }, + "rtt": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socks5": { + "properties": { + "rtt": { + "properties": { + "connect": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "summary": { + "properties": { + "down": { + "type": "long" + }, + "up": { + "type": "long" + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcp": { + "properties": { + "rtt": { + "properties": { + "connect": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tls": { + "properties": { + "certificate_not_valid_after": { + "type": "date" + }, + "certificate_not_valid_before": { + "type": "date" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "rtt": { + "properties": { + "handshake": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/README.md b/x-pack/test/functional/es_archives/rule_exceptions/README.md new file mode 100644 index 0000000000000..1fbf4962d55fe --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/README.md @@ -0,0 +1,11 @@ +Within this folder is input test data for tests such as: + +```ts +security_and_spaces/tests/rule_exceptions.ts +``` + +where these are small ECS compliant input indexes that try to express tests that exercise different parts of +the detection engine around creating and validating that the exceptions part of the detection engine functions. +Compliant meaning that these might contain extra fields but should not clash with ECS. Nothing stopping anyone +from being ECS strict and not having additional extra fields but the extra fields and mappings are to just try +and keep these tests simple and small. diff --git a/x-pack/test/functional/es_archives/rule_exceptions/date/data.json b/x-pack/test/functional/es_archives/rule_exceptions/date/data.json new file mode 100644 index 0000000000000..dd1609070a19d --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/date/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "date", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "date": "2020-10-01T05:08:53.000Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "date", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "date": "2020-10-02T05:08:53.000Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "date", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "date": "2020-10-03T05:08:53.000Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "date", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "date": "2020-10-04T05:08:53.000Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/date/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/date/mappings.json new file mode 100644 index 0000000000000..28c0158cdc852 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/date/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "date", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "date": { "type": "date" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/double/data.json b/x-pack/test/functional/es_archives/rule_exceptions/double/data.json new file mode 100644 index 0000000000000..1f7a5969f5872 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/double/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "double", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "double": 1.0 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "double", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "double": 1.1 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "double", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "double": 1.2 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "double", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "double": 1.3 + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/double/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/double/mappings.json new file mode 100644 index 0000000000000..bd69ae19ed148 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/double/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "double", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "double": { "type": "double" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/double_as_string/data.json b/x-pack/test/functional/es_archives/rule_exceptions/double_as_string/data.json new file mode 100644 index 0000000000000..2bdd685fae4c9 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/double_as_string/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "double_as_string", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "double": "1.0" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "double_as_string", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "double": "1.1" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "double_as_string", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "double": "1.2" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "double_as_string", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "double": "1.3" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/double_as_string/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/double_as_string/mappings.json new file mode 100644 index 0000000000000..a3b3fc52325a5 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/double_as_string/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "double_as_string", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "double": { "type": "double" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/float/data.json b/x-pack/test/functional/es_archives/rule_exceptions/float/data.json new file mode 100644 index 0000000000000..888be5ff20a32 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/float/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "float", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "float": 1.0 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "float", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "float": 1.1 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "float", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "float": 1.2 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "float", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "float": 1.3 + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/float/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/float/mappings.json new file mode 100644 index 0000000000000..b0a7b1a7fc60c --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/float/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "float", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "float": { "type": "float" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/float_as_string/data.json b/x-pack/test/functional/es_archives/rule_exceptions/float_as_string/data.json new file mode 100644 index 0000000000000..4d8575d3ccb9c --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/float_as_string/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "float_as_string", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "float": "1.0" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "float_as_string", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "float": "1.1" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "float_as_string", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "float": "1.2" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "float_as_string", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "float": "1.3" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/float_as_string/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/float_as_string/mappings.json new file mode 100644 index 0000000000000..7e66ace5eb5c6 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/float_as_string/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "float_as_string", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "float": { "type": "float" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/integer/data.json b/x-pack/test/functional/es_archives/rule_exceptions/integer/data.json new file mode 100644 index 0000000000000..5e2f1295397e6 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/integer/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "integer", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "integer": 1 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "integer", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "integer": 2 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "integer", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "integer": 3 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "integer", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "integer": 4 + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/integer/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/integer/mappings.json new file mode 100644 index 0000000000000..a05f3ec4e3186 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/integer/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "integer", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "integer": { "type": "integer" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/integer_as_string/data.json b/x-pack/test/functional/es_archives/rule_exceptions/integer_as_string/data.json new file mode 100644 index 0000000000000..5d0ac56e27d00 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/integer_as_string/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "integer_as_string", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "integer": "1" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "integer_as_string", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "integer": "2" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "integer_as_string", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "integer": "3" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "integer_as_string", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "integer": "4" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/integer_as_string/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/integer_as_string/mappings.json new file mode 100644 index 0000000000000..e98d0d89214dd --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/integer_as_string/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "integer_as_string", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "integer": { "type": "integer" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/ip/data.json b/x-pack/test/functional/es_archives/rule_exceptions/ip/data.json new file mode 100644 index 0000000000000..5dde1cba8f884 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/ip/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "ip", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "ip": "127.0.0.1" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "ip", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "ip": "127.0.0.2" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "ip", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "ip": "127.0.0.3" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "ip", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "ip": "127.0.0.4" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/ip/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/ip/mappings.json new file mode 100644 index 0000000000000..ceb58bc933507 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/ip/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "ip", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "ip": { "type": "ip" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/keyword/data.json b/x-pack/test/functional/es_archives/rule_exceptions/keyword/data.json new file mode 100644 index 0000000000000..09c54843f32c9 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/keyword/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "keyword", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "keyword": "word one" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "keyword", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "keyword": "word two" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "keyword", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "keyword": "word three" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "keyword", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "keyword": "word four" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/keyword/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/keyword/mappings.json new file mode 100644 index 0000000000000..bc8becbe07f45 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/keyword/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "keyword", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "keyword": { "type": "keyword" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/long/data.json b/x-pack/test/functional/es_archives/rule_exceptions/long/data.json new file mode 100644 index 0000000000000..807314bd28173 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/long/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "long", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "long": 1 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "long", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "long": 2 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "long", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "long": 3 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "long", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "long": 4 + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/long/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/long/mappings.json new file mode 100644 index 0000000000000..75b156805af78 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/long/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "long", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "long": { "type": "long" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/long_as_string/data.json b/x-pack/test/functional/es_archives/rule_exceptions/long_as_string/data.json new file mode 100644 index 0000000000000..3604026d2cdb0 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/long_as_string/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "long_as_string", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "long": "1" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "long_as_string", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "long": "2" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "long_as_string", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "long": "3" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "long_as_string", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "long": "4" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/long_as_string/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/long_as_string/mappings.json new file mode 100644 index 0000000000000..8fe9af08127d1 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/long_as_string/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "long_as_string", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "long": { "type": "long" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/text/data.json b/x-pack/test/functional/es_archives/rule_exceptions/text/data.json new file mode 100644 index 0000000000000..8d3da48224cc3 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/text/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "text", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "text": "word one" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "text", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "text": "word two" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "text", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "text": "word three" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "text", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "text": "word four" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/text/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/text/mappings.json new file mode 100644 index 0000000000000..5d3304fc202d5 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/text/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "text", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "text": { "type": "text" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/text_no_spaces/data.json b/x-pack/test/functional/es_archives/rule_exceptions/text_no_spaces/data.json new file mode 100644 index 0000000000000..a0caf9d9eb2d3 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/text_no_spaces/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "text_no_spaces", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "text": "one" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "text_no_spaces", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "text": "two" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "text_no_spaces", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "text": "three" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "text_no_spaces", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "text": "four" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/text_no_spaces/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/text_no_spaces/mappings.json new file mode 100644 index 0000000000000..b981af7937124 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/text_no_spaces/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "text_no_spaces", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "text": { "type": "text" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/wildcard/data.json b/x-pack/test/functional/es_archives/rule_exceptions/wildcard/data.json new file mode 100644 index 0000000000000..40dd24f83c0d2 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/wildcard/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "wildcard", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "wildcard": "word one" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "wildcard", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "wildcard": "word two" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "wildcard": "wildcard", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "wildcard": "word three" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "wildcard", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "wildcard": "word four" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/wildcard/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/wildcard/mappings.json new file mode 100644 index 0000000000000..1b6a697ecbb8f --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/wildcard/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "wildcard", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "wildcard": { "type": "wildcard" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/signals/README.md b/x-pack/test/functional/es_archives/signals/README.md new file mode 100644 index 0000000000000..4b147a414f8b3 --- /dev/null +++ b/x-pack/test/functional/es_archives/signals/README.md @@ -0,0 +1,22 @@ +Within this folder is input test data for tests such as: + +```ts +security_and_spaces/tests/generating_signals.ts +``` + +where these are small ECS compliant input indexes that try to express tests that exercise different parts of +the detection engine signals. Compliant meaning that these might contain extra fields but should not clash with ECS. +Nothing stopping anyone from being ECS strict and not having additional extra fields but the extra fields and mappings +are to just try and keep these tests simple and small. Examples are: + + +This is an ECS document that has a numeric name clash with a signal structure +``` +numeric_name_clash +``` + +This is an ECS document that has an object name clash with a signal structure +``` +object_clash +``` + diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 2d9ee00234bb6..ef80ab475cbd6 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; -import { Role } from '../../../plugins/security/common/model'; +import { AuthenticatedUser, Role } from '../../../plugins/security/common/model'; export function SecurityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); @@ -17,6 +17,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider const esArchiver = getService('esArchiver'); const userMenu = getService('userMenu'); const comboBox = getService('comboBox'); + const supertest = getService('supertestWithoutAuth'); const PageObjects = getPageObjects(['common', 'header', 'error']); interface LoginOptions { @@ -41,10 +42,14 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider }); } + async function isLoginFormVisible() { + return await testSubjects.exists('loginForm'); + } + async function waitForLoginForm() { log.debug('Waiting for Login Form to appear.'); await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => { - return await testSubjects.exists('loginForm'); + return await isLoginFormVisible(); }); } @@ -107,7 +112,9 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider const loginPage = Object.freeze({ async login(username?: string, password?: string, options: LoginOptions = {}) { - await PageObjects.common.navigateToApp('login'); + if (!(await isLoginFormVisible())) { + await PageObjects.common.navigateToApp('login'); + } // ensure welcome screen won't be shown. This is relevant for environments which don't allow // to use the yml setting, e.g. cloud @@ -218,6 +225,21 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider await waitForLoginPage(); } + async getCurrentUser() { + const sidCookie = await browser.getCookie('sid'); + if (!sidCookie?.value) { + log.debug('User is not authenticated yet.'); + return null; + } + + const { body: user } = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', `sid=${sidCookie.value}`) + .expect(200); + return user as AuthenticatedUser; + } + async forceLogout() { log.debug('SecurityPage.forceLogout'); if (await find.existsByDisplayedByCssSelector('.login-form', 100)) { diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 6d8eade25d7e6..1aa6216236827 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -6,6 +6,7 @@ import { services as kibanaFunctionalServices } from '../../../../test/functional/services'; import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; +import { services as kibanaXPackApiIntegrationServices } from '../../api_integration/services'; import { services as commonServices } from '../../common/services'; import { @@ -64,6 +65,7 @@ export const services = { ...commonServices, supertest: kibanaApiIntegrationServices.supertest, + supertestWithoutAuth: kibanaXPackApiIntegrationServices.supertestWithoutAuth, esSupertest: kibanaApiIntegrationServices.esSupertest, monitoringNoData: MonitoringNoDataProvider, monitoringClusterList: MonitoringClusterListProvider, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 1d86d95b7a796..fdcb456493dab 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -9,6 +9,7 @@ import uuid from 'uuid'; import { omit, mapValues, range, flatten } from 'lodash'; import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { alwaysFiringAlertType } from '../../fixtures/plugins/alerts/server/plugin'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); @@ -306,8 +307,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/57426 - describe.skip('Alert Instances', function () { + describe('Alert Instances', function () { const testRunUuid = uuid.v4(); let alert: any; @@ -373,16 +373,31 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // refresh to ensure Api call and UI are looking at freshest output await browser.refresh(); + // Get action groups + const { actionGroups } = alwaysFiringAlertType; + // Verify content await testSubjects.existOrFail('alertInstancesList'); - const summary = await alerting.alerts.getAlertInstanceSummary(alert.id); + const actionGroupNameFromId = (actionGroupId: string) => + actionGroups.find( + (actionGroup: { id: string; name: string }) => actionGroup.id === actionGroupId + )?.name; + const summary = await alerting.alerts.getAlertInstanceSummary(alert.id); const dateOnAllInstancesFromApiResponse = mapValues( summary.instances, (instance) => instance.activeStartDate ); + const actionGroupNameOnAllInstancesFromApiResponse = mapValues( + summary.instances, + (instance) => { + const name = actionGroupNameFromId(instance.actionGroupId); + return name ? ` (${name})` : ''; + } + ); + log.debug( `API RESULT: ${Object.entries(dateOnAllInstancesFromApiResponse) .map(([id, date]) => `${id}: ${moment(date).utc()}`) @@ -393,21 +408,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(instancesList.map((instance) => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', - status: 'Active (Default)', + status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-central']}`, start: moment(dateOnAllInstancesFromApiResponse['us-central']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', - status: 'Active (Default)', + status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-east']}`, start: moment(dateOnAllInstancesFromApiResponse['us-east']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', - status: 'Active (Default)', + status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-west']}`, start: moment(dateOnAllInstancesFromApiResponse['us-west']) .utc() .format('D MMM YYYY @ HH:mm:ss'), diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 6f9d010378624..6584c5891a8b9 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -17,11 +17,62 @@ export interface AlertingExampleDeps { features: FeaturesPluginSetup; } +export const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', +}; + +export const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], + defaultActionGroupId: 'default', + producer: 'alerts', + async executor(alertExecutorOptions: any) { + const { services, state, params } = alertExecutorOptions; + + (params.instances || []).forEach((instance: { id: string; state: any }) => { + services + .alertInstanceFactory(instance.id) + .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) + .scheduleActions('default'); + }); + + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, +}; + +export const failingAlertType: any = { + id: 'test.failing', + name: 'Test: Failing', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alerts', + defaultActionGroupId: 'default', + async executor() { + throw new Error('Failed to execute alert type'); + }, +}; + export class AlertingFixturePlugin implements Plugin<void, void, AlertingExampleDeps> { public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) { - createNoopAlertType(alerts); - createAlwaysFiringAlertType(alerts); - createFailingAlertType(alerts); + alerts.registerType(noopAlertType); + alerts.registerType(alwaysFiringAlertType); + alerts.registerType(failingAlertType); features.registerKibanaFeature({ id: 'alerting_fixture', name: 'alerting_fixture', @@ -56,64 +107,3 @@ export class AlertingFixturePlugin implements Plugin<void, void, AlertingExample public start() {} public stop() {} } - -function createNoopAlertType(alerts: AlertingSetup) { - const noopAlertType: AlertType = { - id: 'test.noop', - name: 'Test: Noop', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - }; - alerts.registerType(noopAlertType); -} - -function createAlwaysFiringAlertType(alerts: AlertingSetup) { - // Alert types - const alwaysFiringAlertType: any = { - id: 'test.always-firing', - name: 'Always Firing', - actionGroups: [ - { id: 'default', name: 'Default' }, - { id: 'other', name: 'Other' }, - ], - defaultActionGroupId: 'default', - producer: 'alerts', - async executor(alertExecutorOptions: any) { - const { services, state, params } = alertExecutorOptions; - - (params.instances || []).forEach((instance: { id: string; state: any }) => { - services - .alertInstanceFactory(instance.id) - .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) - .scheduleActions('default'); - }); - - return { - globalStateValue: true, - groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, - }; - }, - }; - alerts.registerType(alwaysFiringAlertType); -} - -function createFailingAlertType(alerts: AlertingSetup) { - const failingAlertType: any = { - id: 'test.failing', - name: 'Test: Failing', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - producer: 'alerts', - defaultActionGroupId: 'default', - async executor() { - throw new Error('Failed to execute alert type'); - }, - }; - alerts.registerType(failingAlertType); -} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 942b352b4afd3..5ab07aa00412b 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -20,6 +20,7 @@ export interface AlertInstanceSummary { export interface AlertInstanceStatus { status: string; muted: boolean; + actionGroupId: string; activeStartDate?: string; } diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts index 7b7a6173fb408..ae9814e603b74 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts @@ -94,7 +94,7 @@ export default ({ getService }: FtrProviderContext): void => { .get(`${LIST_ITEM_URL}?list_id=list_items.txt&value=127.0.0.1`) .send(); return status !== 404; - }); + }, `${LIST_ITEM_URL}?list_id=list_items.txt&value=127.0.0.1`); const { body } = await supertest .get(`${LIST_ITEM_URL}?list_id=list_items.txt&value=127.0.0.1`) .send() diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 5870239b73ed1..224048e868d7f 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -8,13 +8,15 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Client } from '@elastic/elasticsearch'; +import { getImportListItemAsBuffer } from '../../plugins/lists/common/schemas/request/import_list_item_schema.mock'; import { ListItemSchema, ExceptionListSchema, ExceptionListItemSchema, + Type, } from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; -import { LIST_INDEX } from '../../plugins/lists/common/constants'; +import { LIST_INDEX, LIST_ITEM_URL } from '../../plugins/lists/common/constants'; import { countDownES, countDownTest } from '../detection_engine_api_integration/utils'; /** @@ -109,6 +111,7 @@ export const removeExceptionListServerGeneratedProperties = ( // Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor export const waitFor = async ( functionToTest: () => Promise<boolean>, + functionName: string, maxTimeout: number = 5000, timeoutWait: number = 10 ) => { @@ -127,7 +130,7 @@ export const waitFor = async ( if (found) { resolve(); } else { - reject(new Error('timed out waiting for function condition to be true')); + reject(new Error(`timed out waiting for function ${functionName} condition to be true`)); } }); }; @@ -164,3 +167,134 @@ export const deleteAllExceptions = async (es: Client): Promise<void> => { }); }, 'deleteAllExceptions'); }; + +/** + * Convenience function for quickly importing a given type and contents and then + * waiting to ensure they're there before continuing + * @param supertest The super test agent + * @param type The type to import as + * @param contents The contents of the import + * @param fileName filename to import as + */ +export const importFile = async ( + supertest: SuperTest<supertestAsPromised.Test>, + type: Type, + contents: string[], + fileName: string +): Promise<void> => { + await supertest + .post(`${LIST_ITEM_URL}/_import?type=${type}`) + .set('kbn-xsrf', 'true') + .attach('file', getImportListItemAsBuffer(contents), fileName) + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + + // although we have pushed the list and its items, it is async so we + // have to wait for the contents before continuing + await waitForListItems(supertest, contents, fileName); +}; + +/** + * Convenience function for quickly importing a given type and contents and then + * waiting to ensure they're there before continuing. This specifically checks tokens + * from text file + * @param supertest The super test agent + * @param type The type to import as + * @param contents The contents of the import + * @param fileName filename to import as + */ +export const importTextFile = async ( + supertest: SuperTest<supertestAsPromised.Test>, + type: Type, + contents: string[], + fileName: string +): Promise<void> => { + await supertest + .post(`${LIST_ITEM_URL}/_import?type=${type}`) + .set('kbn-xsrf', 'true') + .attach('file', getImportListItemAsBuffer(contents), fileName) + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + + // although we have pushed the list and its items, it is async so we + // have to wait for the contents before continuing + await waitForTextListItems(supertest, contents, fileName); +}; + +/** + * Convenience function for waiting for a particular file uploaded + * and a particular item value to be available before continuing. + * @param supertest The super test agent + * @param fileName The filename imported + * @param itemValue The item value to wait for + */ +export const waitForListItem = async ( + supertest: SuperTest<supertestAsPromised.Test>, + itemValue: string, + fileName: string +): Promise<void> => { + await waitFor(async () => { + const { status } = await supertest + .get(`${LIST_ITEM_URL}?list_id=${fileName}&value=${itemValue}`) + .send(); + + return status === 200; + }, `waitForListItem fileName: "${fileName}" itemValue: "${itemValue}"`); +}; + +/** + * Convenience function for waiting for a particular file uploaded + * and particular item values to be available before continuing. + * @param supertest The super test agent + * @param fileName The filename imported + * @param itemValue The item value to wait for + */ +export const waitForListItems = async ( + supertest: SuperTest<supertestAsPromised.Test>, + itemValues: string[], + fileName: string +): Promise<void> => { + await Promise.all(itemValues.map((item) => waitForListItem(supertest, item, fileName))); +}; + +/** + * Convenience function for waiting for a particular file uploaded + * and a particular item value to be available before continuing. + * @param supertest The super test agent + * @param fileName The filename imported + * @param itemValue The item value to wait for + */ +export const waitForTextListItem = async ( + supertest: SuperTest<supertestAsPromised.Test>, + itemValue: string, + fileName: string +): Promise<void> => { + const tokens = itemValue.split(' '); + await waitFor(async () => { + const promises = await Promise.all( + tokens.map(async (token) => { + const { status } = await supertest + .get(`${LIST_ITEM_URL}?list_id=${fileName}&value=${token}`) + .send(); + return status === 200; + }) + ); + return promises.every((one) => one); + }, `waitForTextListItem fileName: "${fileName}" itemValue: "${itemValue}"`); +}; + +/** + * Convenience function for waiting for a particular file uploaded + * and particular item values to be available before continuing. This works + * specifically with text types and does tokenization to ensure all words are uploaded + * @param supertest The super test agent + * @param fileName The filename imported + * @param itemValue The item value to wait for + */ +export const waitForTextListItems = async ( + supertest: SuperTest<supertestAsPromised.Test>, + itemValues: string[], + fileName: string +): Promise<void> => { + await Promise.all(itemValues.map((item) => waitForTextListItem(supertest, item, fileName))); +}; diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index 11af83631502b..95f3770443ccb 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -140,33 +140,6 @@ export const getProviderActionsRoute = ( ); }; -export const getLoggerRoute = ( - router: IRouter, - eventLogService: IEventLogService, - logger: Logger -) => { - router.get( - { - path: `/api/log_event_fixture/getEventLogger/{event}`, - validate: { - params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), - }, - }, - async function ( - context: RequestHandlerContext, - req: KibanaRequest<any, any, any, any>, - res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { - const { event } = req.params as { event: string }; - logger.info(`test get event logger for event: ${event}`); - - return res.ok({ - body: { eventLogger: eventLogService.getLogger({ event: { provider: event } }) }, - }); - } - ); -}; - export const isIndexingEntriesRoute = ( router: IRouter, eventLogService: IEventLogService, diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts index 4fb0511db2194..94e5e6faa2b43 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts @@ -11,7 +11,6 @@ import { registerProviderActionsRoute, isProviderActionRegisteredRoute, getProviderActionsRoute, - getLoggerRoute, isIndexingEntriesRoute, isEventLogServiceLoggingEntriesRoute, isEventLogServiceEnabledRoute, @@ -56,7 +55,6 @@ export class EventLogFixturePlugin registerProviderActionsRoute(router, eventLog, this.logger); isProviderActionRegisteredRoute(router, eventLog, this.logger); getProviderActionsRoute(router, eventLog, this.logger); - getLoggerRoute(router, eventLog, this.logger); isIndexingEntriesRoute(router, eventLog, this.logger); isEventLogServiceLoggingEntriesRoute(router, eventLog, this.logger); isEventLogServiceEnabledRoute(router, eventLog, this.logger); diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 5f827dd3eded6..c246e2945a6dd 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -79,18 +79,6 @@ export default function ({ getService }: FtrProviderContext) { expect(providerActions.body.actions).to.be.eql(['action1', 'action2']); }); - it('should allow to get event logger event log service', async () => { - const initResult = await isProviderActionRegistered('provider2', 'action1'); - - if (!initResult.body.isProviderActionRegistered) { - await registerProviderActions('provider2', ['action1', 'action2']); - } - const eventLogger = await getEventLogger('provider2'); - expect(eventLogger.body.eventLogger.initialProperties).to.be.eql({ - event: { provider: 'provider2' }, - }); - }); - it('should allow write an event to index document if indexing entries is enabled', async () => { const initResult = await isProviderActionRegistered('provider4', 'action1'); @@ -138,14 +126,6 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); } - async function getEventLogger(event: string) { - log.debug(`isProviderActionRegistered for event ${event}`); - return await supertest - .get(`/api/log_event_fixture/getEventLogger/${event}`) - .set('kbn-xsrf', 'foo') - .expect(200); - } - async function isIndexingEntries() { log.debug(`isIndexingEntries`); return await supertest diff --git a/x-pack/test/reporting_api_integration/fixtures.ts b/x-pack/test/reporting_api_integration/fixtures.ts index 6d76a158acf1d..afd6ea5582acf 100644 --- a/x-pack/test/reporting_api_integration/fixtures.ts +++ b/x-pack/test/reporting_api_integration/fixtures.ts @@ -251,17 +251,18 @@ export const CSV_RESULT_NANOS_CUSTOM = `date,message,"_id" `; export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,26,569309,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0364103641"",""ZO0708807088""]" "Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,31,569312,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0425104251"",""ZO0107901079""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Shoes""]",EUR,14,569336,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0512505125"",""ZO0384103841""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,28,569337,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0634106341"",""ZO0066900669""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories"",""Men's Clothing""]",EUR,31,569338,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0702507025"",""ZO0528105281""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,27,569356,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0010500105"",""ZO0172201722""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,19,569362,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0292402924"",""ZO0681006810""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,42,569370,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0358603586"",""ZO0641106411""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,20,569371,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0225702257"",""ZO0186601866""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,43,569375,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0347603476"",""ZO0668806688""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,48,569387,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0593805938"",""ZO0125201252""]" `; // This concatenates lines of multi-line string into a single line. diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts index ca3172807139c..20df601f2ff5c 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts @@ -355,7 +355,10 @@ export default function ({ getService }: FtrProviderContext) { timezone: 'UTC', }, state: { - sort: [{ order_date: { order: 'desc', unmapped_type: 'boolean' } }], + sort: [ + { order_date: { order: 'desc', unmapped_type: 'boolean' } }, + { order_id: { order: 'asc', unmapped_type: 'boolean' } }, + ], docvalue_fields: [ { field: 'customer_birth_date', format: 'date_time' }, { field: 'order_date', format: 'date_time' }, diff --git a/x-pack/test/reporting_api_integration/services.ts b/x-pack/test/reporting_api_integration/services.ts index 2c0252fde7693..3b908ecdd2b6e 100644 --- a/x-pack/test/reporting_api_integration/services.ts +++ b/x-pack/test/reporting_api_integration/services.ts @@ -5,8 +5,6 @@ */ import expect from '@kbn/expect'; -import * as Rx from 'rxjs'; -import { filter, first, mapTo, switchMap, timeout } from 'rxjs/operators'; import { indexTimestamp } from '../../plugins/reporting/server/lib/store/index_timestamp'; import { services as xpackServices } from '../functional/services'; import { services as apiIntegrationServices } from '../api_integration/services'; @@ -47,6 +45,7 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { const log = getService('log'); const supertest = getService('supertest'); const esSupertest = getService('esSupertest'); + const retry = getService('retry'); return { async waitForJobToFinish(downloadReportPath: string) { @@ -139,21 +138,12 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { log.debug('ReportingAPI.deleteAllReports'); // ignores 409 errs and keeps retrying - const deleted$ = Rx.interval(100).pipe( - switchMap(() => - esSupertest - .post('/.reporting*/_delete_by_query') - .send({ query: { match_all: {} } }) - .then(({ status }) => status) - ), - filter((status) => status === 200), - mapTo(true), - first(), - timeout(5000) - ); - - const reportsDeleted = await deleted$.toPromise(); - expect(reportsDeleted).to.be(true); + await retry.tryForTime(5000, async () => { + await esSupertest + .post('/.reporting*/_delete_by_query') + .send({ query: { match_all: {} } }) + .expect(200); + }); }, expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) { diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts index d78513ca06206..6bacd5a625a15 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./usage_collection')); }); } diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/usage_collection.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/usage_collection.ts new file mode 100644 index 0000000000000..8804c2cd2ad59 --- /dev/null +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/usage_collection.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const usageAPI = getService('usageAPI'); + + describe('saved_object_tagging usage collector data', () => { + beforeEach(async () => { + await esArchiver.load('usage_collection'); + }); + + afterEach(async () => { + await esArchiver.unload('usage_collection'); + }); + + /* + * Dataset description: + * + * 5 tags: tag-1 tag-2 tag-3 tag-4 ununsed-tag + * 3 dashboard: + * - dash-1: ref to tag-1 + tag-2 + * - dash-2: ref to tag-2 + tag 4 + * - dash-3: no ref to any tag + * 3 visualization: + * - vis-1: ref to tag-1 + * - vis-2: ref to tag-1 + tag-3 + * - vis-3: ref to tag-3 + */ + it('collects the expected data', async () => { + const telemetryStats = (await usageAPI.getTelemetryStats({ + unencrypted: true, + timestamp: Date.now(), + })) as any; + + const taggingStats = telemetryStats[0].stack_stats.kibana.plugins.saved_objects_tagging; + expect(taggingStats).to.eql({ + usedTags: 4, + taggedObjects: 5, + types: { + dashboard: { + taggedObjects: 2, + usedTags: 3, + }, + visualization: { + taggedObjects: 3, + usedTags: 2, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/data.json new file mode 100644 index 0000000000000..a9535ae9e40b2 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/data.json @@ -0,0 +1,313 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "name": "Default Space" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-1", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-1", + "description": "My first tag!", + "color": "#FF00FF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-2", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-2", + "description": "Another awesome tag", + "color": "#FFFFFF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-3", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-3", + "description": "Last but not least", + "color": "#000000" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-4", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-4", + "description": "Last", + "color": "#000000" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:unused-tag", + "index": ".kibana", + "source": { + "tag": { + "name": "unused-tag", + "description": "This tag is unused and should only appear in totalTags", + "color": "#123456" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:ref-to-tag-1", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Vis with ref to tag-1", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [ + { + "type": "tag", + "id": "tag-1", + "name": "tag-1" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:ref-to-tag-1-and-tag-3", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Vis with ref to tag-1 and tag-2", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [ + { + "type": "tag", + "id": "tag-1", + "name": "tag-1" + }, + { + "type": "tag", + "id": "tag-3", + "name": "tag-3" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:ref-to-tag-3", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Vis with ref to tag-2", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [ + { + "type": "tag", + "id": "tag-3", + "name": "tag-3" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-1-and-tag-2", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 1 (tag-2)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-1", + "name": "tag-1-ref", + "type": "tag" + }, + { + "id": "tag-2", + "name": "tag-2-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-2-and-tag-4", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 2 (tag-2 and tag-4)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-2", + "name": "tag-2-ref", + "type": "tag" + }, + { + "id": "tag-4", + "name": "tag-4-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:no-tag-reference", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 2 (tag-2 and tag-4)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/mappings.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/mappings.json new file mode 100644 index 0000000000000..9cf628bef4767 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/mappings.json @@ -0,0 +1,266 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": ".kibana", + "mappings": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "tag": { + "properties": { + "name": { + "type": "text" + }, + "description": { + "type": "text" + }, + "color": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/security_api_integration/anonymous.config.ts b/x-pack/test/security_api_integration/anonymous.config.ts new file mode 100644 index 0000000000000..1742bd09c92f5 --- /dev/null +++ b/x-pack/test/security_api_integration/anonymous.config.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaAPITestsConfig = await readConfigFile( + require.resolve('../../../test/api_integration/config.js') + ); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + return { + testFiles: [require.resolve('./tests/anonymous')], + servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, + services: { + ...kibanaAPITestsConfig.get('services'), + ...xPackAPITestsConfig.get('services'), + }, + junit: { + reportName: 'X-Pack Security API Integration Tests (Anonymous with Username and Password)', + }, + + esTestCluster: { ...xPackAPITestsConfig.get('esTestCluster') }, + + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.security.authc.selector.enabled=false`, + `--xpack.security.authc.providers=${JSON.stringify({ + anonymous: { + anonymous1: { + order: 0, + credentials: { username: 'anonymous_user', password: 'changeme' }, + }, + }, + basic: { basic1: { order: 1 } }, + })}`, + ], + }, + }; +} diff --git a/x-pack/test/security_api_integration/login_selector.config.ts b/x-pack/test/security_api_integration/login_selector.config.ts index 9688d42cb4361..97c7b4334c3b7 100644 --- a/x-pack/test/security_api_integration/login_selector.config.ts +++ b/x-pack/test/security_api_integration/login_selector.config.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { readFileSync } from 'fs'; import { resolve } from 'path'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; @@ -35,6 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kibana: { ...xPackAPITestsConfig.get('servers.kibana'), protocol: 'https', + certificateAuthorities: [readFileSync(CA_CERT_PATH)], }, }; @@ -43,9 +45,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { servers, security: { disableTestUser: true }, services: { - randomness: kibanaAPITestsConfig.get('services.randomness'), - legacyEs: kibanaAPITestsConfig.get('services.legacyEs'), - supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'), + ...kibanaAPITestsConfig.get('services'), + ...xPackAPITestsConfig.get('services'), }, junit: { reportName: 'X-Pack Security API Integration Tests (Login Selector)', @@ -127,6 +128,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { useRelayStateDeepLink: true, }, }, + anonymous: { + anonymous1: { + order: 6, + credentials: { username: 'anonymous_user', password: 'changeme' }, + }, + }, })}`, ], }, diff --git a/x-pack/test/security_api_integration/tests/anonymous/index.ts b/x-pack/test/security_api_integration/tests/anonymous/index.ts new file mode 100644 index 0000000000000..3819d26ae5efa --- /dev/null +++ b/x-pack/test/security_api_integration/tests/anonymous/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('security APIs - Anonymous access', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./login')); + }); +} diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts new file mode 100644 index 0000000000000..e7c876f54ee5a --- /dev/null +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import request, { Cookie } from 'request'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const config = getService('config'); + const security = getService('security'); + + function checkCookieIsSet(cookie: Cookie) { + expect(cookie.value).to.not.be.empty(); + + expect(cookie.key).to.be('sid'); + expect(cookie.path).to.be('/'); + expect(cookie.httpOnly).to.be(true); + expect(cookie.maxAge).to.be(null); + } + + function checkCookieIsCleared(cookie: Cookie) { + expect(cookie.value).to.be.empty(); + + expect(cookie.key).to.be('sid'); + expect(cookie.path).to.be('/'); + expect(cookie.httpOnly).to.be(true); + expect(cookie.maxAge).to.be(0); + } + + describe('Anonymous authentication', () => { + before(async () => { + await security.user.create('anonymous_user', { + password: 'changeme', + roles: [], + full_name: 'Guest', + }); + }); + + after(async () => { + await security.user.delete('anonymous_user'); + }); + + it('should reject API requests if client is not authenticated', async () => { + await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); + }); + + it('does not prevent basic login', async () => { + const [username, password] = config.get('servers.elasticsearch.auth').split(':'); + const response = await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic1', + currentURL: '/', + params: { username, password }, + }) + .expect(200); + + const cookies = response.headers['set-cookie']; + expect(cookies).to.have.length(1); + + const cookie = request.cookie(cookies[0])!; + checkCookieIsSet(cookie); + + const { body: user } = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', cookie.cookieString()) + .expect(200); + + expect(user.username).to.eql(username); + expect(user.authentication_provider).to.eql({ type: 'basic', name: 'basic1' }); + expect(user.authentication_type).to.eql('realm'); + // Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud + }); + + describe('login', () => { + it('should properly set cookie and authenticate user', async () => { + const response = await supertest.get('/security/account').expect(200); + + const cookies = response.headers['set-cookie']; + expect(cookies).to.have.length(1); + + const sessionCookie = request.cookie(cookies[0])!; + checkCookieIsSet(sessionCookie); + + const { body: user } = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .expect(200); + + expect(user.username).to.eql('anonymous_user'); + expect(user.authentication_provider).to.eql({ type: 'anonymous', name: 'anonymous1' }); + expect(user.authentication_type).to.eql('realm'); + // Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud + }); + + it('should fail if `Authorization` header is present, but not valid', async () => { + const response = await supertest + .get('/security/account') + .set('Authorization', 'Basic wow') + .expect(401); + expect(response.headers['set-cookie']).to.be(undefined); + }); + }); + + describe('API access with active session', () => { + let sessionCookie: Cookie; + + beforeEach(async () => { + const response = await supertest.get('/security/account').expect(200); + + const cookies = response.headers['set-cookie']; + expect(cookies).to.have.length(1); + + sessionCookie = request.cookie(cookies[0])!; + checkCookieIsSet(sessionCookie); + }); + + it('should not extend cookie for system AND non-system API calls', async () => { + const apiResponseOne = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .expect(200); + + expect(apiResponseOne.headers['set-cookie']).to.be(undefined); + + const systemAPIResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-request', 'true') + .set('Cookie', sessionCookie.cookieString()) + .expect(200); + + expect(systemAPIResponse.headers['set-cookie']).to.be(undefined); + }); + + it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { + const apiResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Authorization', 'Basic a3JiNTprcmI1') + .set('Cookie', sessionCookie.cookieString()) + .expect(401); + + expect(apiResponse.headers['set-cookie']).to.be(undefined); + }); + }); + + describe('logging out', () => { + it('should redirect to `logged_out` page after successful logout', async () => { + // First authenticate user to retrieve session cookie. + const response = await supertest.get('/security/account').expect(200); + let cookies = response.headers['set-cookie']; + expect(cookies).to.have.length(1); + + const sessionCookie = request.cookie(cookies[0])!; + checkCookieIsSet(sessionCookie); + + // And then log user out. + const logoutResponse = await supertest + .get('/api/security/logout') + .set('Cookie', sessionCookie.cookieString()) + .expect(302); + + cookies = logoutResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + checkCookieIsCleared(request.cookie(cookies[0])!); + + expect(logoutResponse.headers.location).to.be('/security/logged_out'); + + // Old cookie should be invalidated and not allow API access. + const apiResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .expect(401); + + // If Kibana detects cookie with invalid token it tries to clear it. + cookies = apiResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + checkCookieIsCleared(request.cookie(cookies[0])!); + }); + + it('should redirect to home page if session cookie is not provided', async () => { + const logoutResponse = await supertest.get('/api/security/logout').expect(302); + + expect(logoutResponse.headers['set-cookie']).to.be(undefined); + expect(logoutResponse.headers.location).to.be('/'); + }); + }); + }); +} diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts index cf141972b044a..edcc1b5744fe3 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts @@ -23,6 +23,7 @@ export default function ({ getService }: FtrProviderContext) { const randomness = getService('randomness'); const supertest = getService('supertestWithoutAuth'); const config = getService('config'); + const security = getService('security'); const kibanaServerConfig = config.get('servers.kibana'); const validUsername = kibanaServerConfig.username; @@ -748,5 +749,68 @@ export default function ({ getService }: FtrProviderContext) { ); }); }); + + describe('Anonymous', () => { + before(async () => { + await security.user.create('anonymous_user', { + password: 'changeme', + roles: [], + full_name: 'Guest', + }); + }); + + after(async () => { + await security.user.delete('anonymous_user'); + }); + + it('should be able to log in from Login Selector', async () => { + const authenticationResponse = await supertest + .post('/internal/security/login') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'anonymous', + providerName: 'anonymous1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(200); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie( + request.cookie(cookies[0])!, + 'anonymous_user', + { type: 'anonymous', name: 'anonymous1' }, + { name: 'native1', type: 'native' }, + 'realm' + ); + }); + + it('should be able to log in from Login Selector even if client provides certificate and PKI is enabled', async () => { + const authenticationResponse = await supertest + .post('/internal/security/login') + .ca(CA_CERT) + .pfx(CLIENT_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'anonymous', + providerName: 'anonymous1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(200); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie( + request.cookie(cookies[0])!, + 'anonymous_user', + { type: 'anonymous', name: 'anonymous1' }, + { name: 'native1', type: 'native' }, + 'realm' + ); + }); + }); }); } diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index 9fc4c54ba1344..2ee47491c5ff3 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -42,7 +42,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { from: 'snapshot', serverArgs: [ 'xpack.security.authc.token.enabled=true', - 'xpack.security.authc.realms.saml.saml1.order=0', + 'xpack.security.authc.realms.native.native1.order=0', + 'xpack.security.authc.realms.saml.saml1.order=1', `xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`, 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1', `xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`, @@ -60,15 +61,29 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.security.loginHelp="Some-login-help."`, - '--xpack.security.authc.providers.basic.basic1.order=0', - '--xpack.security.authc.providers.saml.saml1.order=1', - '--xpack.security.authc.providers.saml.saml1.realm=saml1', - '--xpack.security.authc.providers.saml.saml1.description="Log-in-with-SAML"', - '--xpack.security.authc.providers.saml.saml1.icon=logoKibana', - '--xpack.security.authc.providers.saml.unknown_saml.order=2', - '--xpack.security.authc.providers.saml.unknown_saml.realm=unknown_realm', - '--xpack.security.authc.providers.saml.unknown_saml.description="Do-not-log-in-with-THIS-SAML"', - '--xpack.security.authc.providers.saml.unknown_saml.icon=logoAWS', + `--xpack.security.authc.providers=${JSON.stringify({ + basic: { basic1: { order: 0 } }, + saml: { + saml1: { + order: 1, + realm: 'saml1', + description: 'Log-in-with-SAML', + icon: 'logoKibana', + }, + unknown_saml: { + order: 2, + realm: 'unknown_realm', + description: 'Do-not-log-in-with-THIS-SAML', + icon: 'logoAWS', + }, + }, + anonymous: { + anonymous1: { + order: 3, + credentials: { username: 'anonymous_user', password: 'changeme' }, + }, + }, + })}`, ], }, uiSettings: { diff --git a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts new file mode 100644 index 0000000000000..8c20862559092 --- /dev/null +++ b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { parse } from 'url'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const security = getService('security'); + const PageObjects = getPageObjects(['security', 'common']); + + describe('Authentication provider hint', function () { + this.tags('includeFirefox'); + + before(async () => { + await getService('esSupertest') + .post('/_security/role_mapping/saml1') + .send({ roles: ['superuser'], enabled: true, rules: { field: { 'realm.name': 'saml1' } } }) + .expect(200); + + await security.user.create('anonymous_user', { + password: 'changeme', + roles: ['superuser'], + full_name: 'Guest', + }); + + await esArchiver.load('../../functional/es_archives/empty_kibana'); + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await security.user.delete('anonymous_user'); + await esArchiver.unload('../../functional/es_archives/empty_kibana'); + }); + + beforeEach(async () => { + await browser.get(`${PageObjects.common.getHostPort()}/login`); + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + }); + + afterEach(async () => { + await PageObjects.security.forceLogout(); + }); + + it('automatically activates Login Form preserving original URL', async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'management', + '/security/users', + '?auth_provider_hint=basic1', + { ensureCurrentUrl: false, shouldLoginIfPrompted: false } + ); + await PageObjects.common.waitUntilUrlIncludes('next='); + + // Login form should be automatically activated by the auth provider hint. + await PageObjects.security.loginSelector.verifyLoginFormIsVisible(); + await PageObjects.security.loginPage.login(undefined, undefined, { expectSuccess: true }); + + const currentURL = parse(await browser.getCurrentUrl()); + expect(currentURL.pathname).to.eql('/app/management/security/users'); + expect((await PageObjects.security.getCurrentUser())?.authentication_provider).to.eql({ + type: 'basic', + name: 'basic1', + }); + }); + + it('automatically login with SSO preserving original URL', async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'management', + '/security/users', + '?auth_provider_hint=saml1', + { ensureCurrentUrl: false, shouldLoginIfPrompted: false } + ); + + await PageObjects.common.waitUntilUrlIncludes('/app/management/security/users'); + + const currentURL = parse(await browser.getCurrentUrl()); + expect(currentURL.pathname).to.eql('/app/management/security/users'); + expect((await PageObjects.security.getCurrentUser())?.authentication_provider).to.eql({ + type: 'saml', + name: 'saml1', + }); + }); + + it('can login anonymously preserving original URL', async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'management', + '/security/users', + '?auth_provider_hint=anonymous1', + { ensureCurrentUrl: false, shouldLoginIfPrompted: false } + ); + + await PageObjects.common.waitUntilUrlIncludes('/app/management/security/users'); + + const currentURL = parse(await browser.getCurrentUrl()); + expect(currentURL.pathname).to.eql('/app/management/security/users'); + expect((await PageObjects.security.getCurrentUser())?.authentication_provider).to.eql({ + type: 'anonymous', + name: 'anonymous1', + }); + }); + }); +} diff --git a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts index 153387c52e5c3..a08fae4cdb0a1 100644 --- a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts @@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); + const security = getService('security'); const PageObjects = getPageObjects(['security', 'common']); describe('Basic functionality', function () { @@ -71,6 +72,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(currentURL.pathname).to.eql('/app/management/security/users'); }); + it('can login anonymously preserving original URL', async () => { + await PageObjects.common.navigateToUrl('management', 'security/users', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + await PageObjects.common.waitUntilUrlIncludes('next='); + + await security.user.create('anonymous_user', { + password: 'changeme', + roles: ['superuser'], + full_name: 'Guest', + }); + await PageObjects.security.loginSelector.login('anonymous', 'anonymous1'); + await security.user.delete('anonymous_user'); + + // We need to make sure that both path and hash are respected. + const currentURL = parse(await browser.getCurrentUrl()); + expect(currentURL.pathname).to.eql('/app/management/security/users'); + }); + it('should show toast with error if SSO fails', async () => { await PageObjects.security.loginSelector.selectLoginMethod('saml', 'unknown_saml'); @@ -80,6 +102,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); }); + it('should show toast with error if anonymous login fails', async () => { + await PageObjects.security.loginSelector.selectLoginMethod('anonymous', 'anonymous1'); + + const toastTitle = await PageObjects.common.closeToast(); + expect(toastTitle).to.eql('Could not perform login.'); + + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + }); + it('can go to Login Form and return back to Selector', async () => { await PageObjects.security.loginSelector.selectLoginMethod('basic', 'basic1'); await PageObjects.security.loginSelector.verifyLoginFormIsVisible(); diff --git a/x-pack/test/security_functional/tests/login_selector/index.ts b/x-pack/test/security_functional/tests/login_selector/index.ts index 0d1060fbf1f51..ee25e365d495d 100644 --- a/x-pack/test/security_functional/tests/login_selector/index.ts +++ b/x-pack/test/security_functional/tests/login_selector/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup4'); loadTestFile(require.resolve('./basic_functionality')); + loadTestFile(require.resolve('./auth_provider_hint')); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index f032416d2e7bb..b3c130ea1e5dc 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -52,6 +52,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { policyInfo.packagePolicy.name ); }); + + it('and the show advanced settings button is clicked', async () => { + await testSubjects.missingOrFail('advancedPolicyPanel'); + + let advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + + await testSubjects.existOrFail('advancedPolicyPanel'); + + advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + await testSubjects.missingOrFail('advancedPolicyPanel'); + }); }); describe('and the save button is clicked', () => { @@ -98,7 +111,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyLinuxEvent_file'), pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyMacEvent_file'), ]); + + const advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + + const advancedPolicyField = await pageObjects.policy.findAdvancedPolicyField(); + await advancedPolicyField.clearValue(); + await advancedPolicyField.click(); + await advancedPolicyField.type('true'); await pageObjects.policy.confirmAndSave(); + await testSubjects.existOrFail('policyDetailsSuccessMessage'); const agentFullPolicy = await policyTestResources.getFullAgentPolicy( @@ -191,6 +213,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { linux: { events: { file: false, network: true, process: true }, logging: { file: 'info' }, + advanced: { agent: { connection_delay: 'true' } }, }, mac: { events: { file: false, network: true, process: true }, diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index 92571e5c27566..8bfbdc32452ee 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -77,6 +77,22 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.find('policyDetailsCancelButton'); }, + /** + * Finds and returns the Advanced Policy Show/Hide Button + */ + async findAdvancedPolicyButton() { + await this.ensureIsOnDetailsPage(); + return await testSubjects.find('advancedPolicyButton'); + }, + + /** + * Finds and returns the linux connection_delay Advanced Policy field + */ + async findAdvancedPolicyField() { + await this.ensureIsOnDetailsPage(); + return await testSubjects.find('linux.advanced.agent.connection_delay'); + }, + /** * ensures that the Details Page is the currently display view */ diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 804268fbf5dac..12782e6bdd5ea 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,17 +22,21 @@ }, "references": [ { "path": "../src/core/tsconfig.json" }, + { "path": "../src/plugins/dev_tools/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, { "path": "../src/plugins/kibana_legacy/tsconfig.json" }, { "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/newsfeed/tsconfig.json" }, + { "path": "../src/plugins/security_oss/tsconfig.json" }, { "path": "../src/plugins/share/tsconfig.json" }, - { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../src/test_utils/tsconfig.json" }, + { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" } diff --git a/x-pack/plugins/apm/typings/cytoscape_dagre.d.ts b/x-pack/typings/cytoscape_dagre.d.ts similarity index 100% rename from x-pack/plugins/apm/typings/cytoscape_dagre.d.ts rename to x-pack/typings/cytoscape_dagre.d.ts diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index bc9ed447c8717..f471b83fbbc6b 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -354,6 +354,7 @@ interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOpti bg_count: number; buckets: Array< { + score: number; bg_count: number; doc_count: number; key: string | number; diff --git a/x-pack/plugins/apm/typings/react_vis.d.ts b/x-pack/typings/react_vis.d.ts similarity index 100% rename from x-pack/plugins/apm/typings/react_vis.d.ts rename to x-pack/typings/react_vis.d.ts diff --git a/yarn.lock b/yarn.lock index f6000d1fedec7..021a7f5480606 100644 --- a/yarn.lock +++ b/yarn.lock @@ -613,10 +613,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" - integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== +"@babel/plugin-syntax-top-level-await@^7.10.4", "@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" @@ -2058,61 +2058,61 @@ chalk "^2.0.1" slash "^2.0.0" -"@jest/console@^26.3.0", "@jest/console@^26.5.2": - version "26.5.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.5.2.tgz#94fc4865b1abed7c352b5e21e6c57be4b95604a6" - integrity sha512-lJELzKINpF1v74DXHbCRIkQ/+nUV1M+ntj+X1J8LxCgpmJZjfLmhFejiMSbjjD66fayxl5Z06tbs3HMyuik6rw== +"@jest/console@^26.5.2", "@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.5.2" - jest-util "^26.5.2" + jest-message-util "^26.6.2" + jest-util "^26.6.2" slash "^3.0.0" -"@jest/core@^26.4.2": - version "26.4.2" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.4.2.tgz#85d0894f31ac29b5bab07aa86806d03dd3d33edc" - integrity sha512-sDva7YkeNprxJfepOctzS8cAk9TOekldh+5FhVuXS40+94SHbiicRO1VV2tSoRtgIo+POs/Cdyf8p76vPTd6dg== +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== dependencies: - "@jest/console" "^26.3.0" - "@jest/reporters" "^26.4.1" - "@jest/test-result" "^26.3.0" - "@jest/transform" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^26.3.0" - jest-config "^26.4.2" - jest-haste-map "^26.3.0" - jest-message-util "^26.3.0" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" jest-regex-util "^26.0.0" - jest-resolve "^26.4.0" - jest-resolve-dependencies "^26.4.2" - jest-runner "^26.4.2" - jest-runtime "^26.4.2" - jest-snapshot "^26.4.2" - jest-util "^26.3.0" - jest-validate "^26.4.2" - jest-watcher "^26.3.0" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" micromatch "^4.0.2" p-each-series "^2.1.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.3.0.tgz#e6953ab711ae3e44754a025f838bde1a7fd236a0" - integrity sha512-EW+MFEo0DGHahf83RAaiqQx688qpXgl99wdb8Fy67ybyzHwR1a58LHcO376xQJHfmoXTu89M09dH3J509cx2AA== +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== dependencies: - "@jest/fake-timers" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.3.0" + jest-mock "^26.6.2" "@jest/fake-timers@^24.9.0": version "24.9.0" @@ -2123,31 +2123,31 @@ jest-message-util "^24.9.0" jest-mock "^24.9.0" -"@jest/fake-timers@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.3.0.tgz#f515d4667a6770f60ae06ae050f4e001126c666a" - integrity sha512-ZL9ytUiRwVP8ujfRepffokBvD2KbxbqMhrXSBhSdAhISCw3gOkuntisiSFv+A6HN0n0fF4cxzICEKZENLmW+1A== +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" "@sinonjs/fake-timers" "^6.0.1" "@types/node" "*" - jest-message-util "^26.3.0" - jest-mock "^26.3.0" - jest-util "^26.3.0" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" -"@jest/globals@^26.4.2": - version "26.4.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.4.2.tgz#73c2a862ac691d998889a241beb3dc9cada40d4a" - integrity sha512-Ot5ouAlehhHLRhc+sDz2/9bmNv9p5ZWZ9LE1pXGGTCXBasmi5jnYjlgYcYt03FBwLmZXCZ7GrL29c33/XRQiow== +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== dependencies: - "@jest/environment" "^26.3.0" - "@jest/types" "^26.3.0" - expect "^26.4.2" + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" -"@jest/reporters@^26.4.1": - version "26.5.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.5.2.tgz#0f1c900c6af712b46853d9d486c9c0382e4050f6" - integrity sha512-zvq6Wvy6MmJq/0QY0YfOPb49CXKSf42wkJbrBPkeypVa8I+XDxijvFuywo6TJBX/ILPrdrlE/FW9vJZh6Rf9vA== +"@jest/reporters@^26.5.2": + version "26.5.3" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.5.3.tgz#e810e9c2b670f33f1c09e9975749260ca12f1c17" + integrity sha512-X+vR0CpfMQzYcYmMFKNY9n4jklcb14Kffffp7+H/MqitWnb0440bW2L76NGWKAa+bnXhNoZr+lCVtdtPmfJVOQ== dependencies: "@bcoe/v8-coverage" "^0.2.3" "@jest/console" "^26.5.2" @@ -2172,20 +2172,20 @@ source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^5.0.1" + v8-to-istanbul "^6.0.1" optionalDependencies: node-notifier "^8.0.0" -"@jest/reporters@^26.5.2": - version "26.5.3" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.5.3.tgz#e810e9c2b670f33f1c09e9975749260ca12f1c17" - integrity sha512-X+vR0CpfMQzYcYmMFKNY9n4jklcb14Kffffp7+H/MqitWnb0440bW2L76NGWKAa+bnXhNoZr+lCVtdtPmfJVOQ== +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.5.2" - "@jest/test-result" "^26.5.2" - "@jest/transform" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -2196,15 +2196,15 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.5.2" - jest-resolve "^26.5.2" - jest-util "^26.5.2" - jest-worker "^26.5.0" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^6.0.1" + v8-to-istanbul "^7.0.0" optionalDependencies: node-notifier "^8.0.0" @@ -2217,10 +2217,10 @@ graceful-fs "^4.1.15" source-map "^0.6.0" -"@jest/source-map@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.3.0.tgz#0e646e519883c14c551f7b5ae4ff5f1bfe4fc3d9" - integrity sha512-hWX5IHmMDWe1kyrKl7IhFwqOuAreIwHhbe44+XH2ZRHjrKIh0LO5eLQ/vxHFeAfRwJapmxuqlGAEYLadDq6ZGQ== +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" @@ -2235,52 +2235,42 @@ "@jest/types" "^24.9.0" "@types/istanbul-lib-coverage" "^2.0.0" -"@jest/test-result@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.3.0.tgz#46cde01fa10c0aaeb7431bf71e4a20d885bc7fdb" - integrity sha512-a8rbLqzW/q7HWheFVMtghXV79Xk+GWwOK1FrtimpI5n1la2SY0qHri3/b0/1F0Ve0/yJmV8pEhxDfVwiUBGtgg== - dependencies: - "@jest/console" "^26.3.0" - "@jest/types" "^26.3.0" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-result@^26.5.2": - version "26.5.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.5.2.tgz#cc1a44cfd4db2ecee3fb0bc4e9fe087aa54b5230" - integrity sha512-E/Zp6LURJEGSCWpoMGmCFuuEI1OWuI3hmZwmULV0GsgJBh7u0rwqioxhRU95euUuviqBDN8ruX/vP/4bwYolXw== +"@jest/test-result@^26.5.2", "@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== dependencies: - "@jest/console" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.4.2": - version "26.4.2" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.4.2.tgz#58a3760a61eec758a2ce6080201424580d97cbba" - integrity sha512-83DRD8N3M0tOhz9h0bn6Kl6dSp+US6DazuVF8J9m21WAp5x7CqSMaNycMP0aemC/SH/pDQQddbsfHRTBXVUgog== +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== dependencies: - "@jest/test-result" "^26.3.0" + "@jest/test-result" "^26.6.2" graceful-fs "^4.2.4" - jest-haste-map "^26.3.0" - jest-runner "^26.4.2" - jest-runtime "^26.4.2" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" -"@jest/transform@^26.0.0", "@jest/transform@^26.3.0", "@jest/transform@^26.5.2": - version "26.5.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.5.2.tgz#6a0033a1d24316a1c75184d010d864f2c681bef5" - integrity sha512-AUNjvexh+APhhmS8S+KboPz+D3pCxPvEAGduffaAJYxIFxGi/ytZQkrqcKDUU0ERBAo5R7087fyOYr2oms1seg== +"@jest/transform@^26.0.0", "@jest/transform@^26.5.2", "@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.5.2" + jest-haste-map "^26.6.2" jest-regex-util "^26.0.0" - jest-util "^26.5.2" + jest-util "^26.6.2" micromatch "^4.0.2" pirates "^4.0.1" slash "^3.0.0" @@ -2306,10 +2296,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^26.3.0", "@jest/types@^26.5.2": - version "26.5.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.5.2.tgz#44c24f30c8ee6c7f492ead9ec3f3c62a5289756d" - integrity sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg== +"@jest/types@^26.5.2", "@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -2667,6 +2657,10 @@ version "0.0.0" uid "" +"@kbn/legacy-logging@link:packages/kbn-legacy-logging": + version "0.0.0" + uid "" + "@kbn/logging@link:packages/kbn-logging": version "0.0.0" uid "" @@ -4373,10 +4367,10 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" - integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" + integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== dependencies: "@babel/types" "^7.3.0" @@ -4495,11 +4489,6 @@ dependencies: "@types/webpack" "*" -"@types/console-stamp@^0.2.32": - version "0.2.32" - resolved "https://registry.yarnpkg.com/@types/console-stamp/-/console-stamp-0.2.32.tgz#9cb9dce41b6203a28486365300a8a1cf99e5801f" - integrity sha512-Ih8HUSWSNtmHf5DgLv+BZGKaNGZKOaFjkxb/nHOBfc2wLrWY5wFQq6rjLu+LxCD/Mc+8GoKhby364Bu0Be25tQ== - "@types/cookiejar@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" @@ -7656,16 +7645,16 @@ babel-helper-to-multiple-sequence-expressions@^0.5.0: resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d" integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA== -babel-jest@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.3.0.tgz#10d0ca4b529ca3e7d1417855ef7d7bd6fc0c3463" - integrity sha512-sxPnQGEyHAOPF8NcUsD0g7hDCnvLL2XyblRBcgrzTWBB/mAIpWow3n1bEL+VghnnZfreLhFSBsFluRoK2tRK4g== +babel-jest@^26.3.0, babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== dependencies: - "@jest/transform" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.3.0" + babel-preset-jest "^26.6.2" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" @@ -7748,10 +7737,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.2.0: - version "26.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.2.0.tgz#bdd0011df0d3d513e5e95f76bd53b51147aca2dd" - integrity sha512-B/hVMRv8Nh1sQ1a3EY8I0n4Y1Wty3NrR5ebOyVT302op+DOAau+xNEImGMsUWOC3++ZlMooCytKz+NgN8aKGbA== +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -7948,10 +7937,10 @@ babel-polyfill@^6.26.0: core-js "^2.5.0" regenerator-runtime "^0.10.5" -babel-preset-current-node-syntax@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da" - integrity sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ== +babel-preset-current-node-syntax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" + integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -7964,14 +7953,15 @@ babel-preset-current-node-syntax@^0.1.3: "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.3.0.tgz#ed6344506225c065fd8a0b53e191986f74890776" - integrity sha512-5WPdf7nyYi2/eRxCbVrE1kKCWxgWY4RsPEbdJWFm7QsesFGqjdkyLeu1zRkwM1cxK6EPIlNd6d2AxLk7J+t4pw== +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== dependencies: - babel-plugin-jest-hoist "^26.2.0" - babel-preset-current-node-syntax "^0.1.3" + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" "babel-preset-minify@^0.5.0 || 0.6.0-alpha.5": version "0.5.0" @@ -9403,6 +9393,11 @@ circular-json@^0.3.1: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + class-utils@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.5.tgz#17e793103750f9627b2176ea34cfd1b565903c80" @@ -10038,15 +10033,6 @@ console-log-level@^1.4.1: resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" integrity sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ== -console-stamp@^0.2.9: - version "0.2.9" - resolved "https://registry.yarnpkg.com/console-stamp/-/console-stamp-0.2.9.tgz#9c0cd206d1fd60dec4e263ddeebde2469209c99f" - integrity sha512-jtgd1Fx3Im+pWN54mF269ptunkzF5Lpct2LBTbtyNoK2A4XjcxLM+TQW+e+XE/bLwLQNGRqPqlxm9JMixFntRA== - dependencies: - chalk "^1.1.1" - dateformat "^1.0.11" - merge "^1.2.0" - constant-case@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" @@ -11105,14 +11091,6 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -dateformat@^1.0.11: - version "1.0.12" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" - integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= - dependencies: - get-stdin "^4.0.1" - meow "^3.3.0" - dateformat@^3.0.2, dateformat@~3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -11697,10 +11675,10 @@ diff-sequences@^25.2.6: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== -diff-sequences@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.3.0.tgz#62a59b1b29ab7fd27cef2a33ae52abe73042d0a2" - integrity sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== diff@3.5.0, diff@^3.0.0, diff@^3.5.0: version "3.5.0" @@ -13190,16 +13168,16 @@ expect@^24.8.0, expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" -expect@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.4.2.tgz#36db120928a5a2d7d9736643032de32f24e1b2a1" - integrity sha512-IlJ3X52Z0lDHm7gjEp+m76uX46ldH5VpqmU0006vqDju/285twh7zaWMRhs67VpQhBwjjMchk+p5aA0VkERCAA== +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" ansi-styles "^4.0.0" jest-get-type "^26.3.0" - jest-matcher-utils "^26.4.2" - jest-message-util "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" jest-regex-util "^26.0.0" expiry-js@0.1.7: @@ -16605,6 +16583,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-core-module@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -17382,83 +17367,84 @@ jest-canvas-mock@^2.2.0: cssfontparser "^1.2.1" parse-color "^1.0.0" -jest-changed-files@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.3.0.tgz#68fb2a7eb125f50839dab1f5a17db3607fe195b1" - integrity sha512-1C4R4nijgPltX6fugKxM4oQ18zimS7LqQ+zTTY8lMCMFPrxqBFb7KJH0Z2fRQJvw2Slbaipsqq7s1mgX5Iot+g== +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" execa "^4.0.0" throat "^5.0.0" -jest-circus@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-26.4.2.tgz#f84487d2ea635cadf1feb269b14ad0602135ad17" - integrity sha512-gzxoteivskdUTNxT7Jx6hrANsEm+x1wh8jaXmQCtzC7zoNWirk9chYdSosHFC4tJlfDZa0EsPreVAxLicLsV0w== +jest-circus@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-26.6.3.tgz#3cc7ef2a6a3787e5d7bfbe2c72d83262154053e7" + integrity sha512-ACrpWZGcQMpbv13XbzRzpytEJlilP/Su0JtNCi5r/xLpOUhnaIJr8leYYpLEMgPFURZISEHrnnpmB54Q/UziPw== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.3.0" - "@jest/test-result" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^26.4.2" + expect "^26.6.2" is-generator-fn "^2.0.0" - jest-each "^26.4.2" - jest-matcher-utils "^26.4.2" - jest-message-util "^26.3.0" - jest-runner "^26.4.2" - jest-runtime "^26.4.2" - jest-snapshot "^26.4.2" - jest-util "^26.3.0" - pretty-format "^26.4.2" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" stack-utils "^2.0.2" throat "^5.0.0" -jest-cli@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.4.2.tgz#24afc6e4dfc25cde4c7ec4226fb7db5f157c21da" - integrity sha512-zb+lGd/SfrPvoRSC/0LWdaWCnscXc1mGYW//NP4/tmBvRPT3VntZ2jtKUONsRi59zc5JqmsSajA9ewJKFYp8Cw== +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== dependencies: - "@jest/core" "^26.4.2" - "@jest/test-result" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^26.4.2" - jest-util "^26.3.0" - jest-validate "^26.4.2" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" prompts "^2.0.1" - yargs "^15.3.1" + yargs "^15.4.1" -jest-config@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.4.2.tgz#da0cbb7dc2c131ffe831f0f7f2a36256e6086558" - integrity sha512-QBf7YGLuToiM8PmTnJEdRxyYy3mHWLh24LJZKVdXZ2PNdizSe1B/E8bVm+HYcjbEzGuVXDv/di+EzdO/6Gq80A== +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.4.2" - "@jest/types" "^26.3.0" - babel-jest "^26.3.0" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" chalk "^4.0.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-environment-jsdom "^26.3.0" - jest-environment-node "^26.3.0" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" jest-get-type "^26.3.0" - jest-jasmine2 "^26.4.2" + jest-jasmine2 "^26.6.3" jest-regex-util "^26.0.0" - jest-resolve "^26.4.0" - jest-util "^26.3.0" - jest-validate "^26.4.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" micromatch "^4.0.2" - pretty-format "^26.4.2" + pretty-format "^26.6.2" jest-diff@^24.9.0: version "24.9.0" @@ -17480,15 +17466,15 @@ jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.5.0" -jest-diff@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.4.2.tgz#a1b7b303bcc534aabdb3bd4a7caf594ac059f5aa" - integrity sha512-6T1XQY8U28WH0Z5rGpQ+VqZSZz8EN8rZcBtfvXaOkbwxIEeRre6qnuZQlbY1AJ4MKDxQF8EkrCvK+hL/VkyYLQ== +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== dependencies: chalk "^4.0.0" - diff-sequences "^26.3.0" + diff-sequences "^26.6.2" jest-get-type "^26.3.0" - pretty-format "^26.4.2" + pretty-format "^26.6.2" jest-docblock@^26.0.0: version "26.0.0" @@ -17497,16 +17483,16 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.4.2.tgz#bb14f7f4304f2bb2e2b81f783f989449b8b6ffae" - integrity sha512-p15rt8r8cUcRY0Mvo1fpkOGYm7iI8S6ySxgIdfh3oOIv+gHwrHTy5VWCGOecWUhDsit4Nz8avJWdT07WLpbwDA== +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" chalk "^4.0.0" jest-get-type "^26.3.0" - jest-util "^26.3.0" - pretty-format "^26.4.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" jest-environment-jsdom-thirteen@^1.0.1: version "1.0.1" @@ -17517,30 +17503,30 @@ jest-environment-jsdom-thirteen@^1.0.1: jest-util "^24.0.0" jsdom "^13.0.0" -jest-environment-jsdom@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.3.0.tgz#3b749ba0f3a78e92ba2c9ce519e16e5dd515220c" - integrity sha512-zra8He2btIMJkAzvLaiZ9QwEPGEetbxqmjEBQwhH3CA+Hhhu0jSiEJxnJMbX28TGUvPLxBt/zyaTLrOPF4yMJA== +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== dependencies: - "@jest/environment" "^26.3.0" - "@jest/fake-timers" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.3.0" - jest-util "^26.3.0" - jsdom "^16.2.2" - -jest-environment-node@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.3.0.tgz#56c6cfb506d1597f94ee8d717072bda7228df849" - integrity sha512-c9BvYoo+FGcMj5FunbBgtBnbR5qk3uky8PKyRVpSfe2/8+LrNQMiXX53z6q2kY+j15SkjQCOSL/6LHnCPLVHNw== - dependencies: - "@jest/environment" "^26.3.0" - "@jest/fake-timers" "^26.3.0" - "@jest/types" "^26.3.0" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jsdom "^16.4.0" + +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.3.0" - jest-util "^26.3.0" + jest-mock "^26.6.2" + jest-util "^26.6.2" jest-get-type@^24.9.0: version "24.9.0" @@ -17557,58 +17543,58 @@ jest-get-type@^26.3.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.3.0, jest-haste-map@^26.5.2: - version "26.5.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.5.2.tgz#a15008abfc502c18aa56e4919ed8c96304ceb23d" - integrity sha512-lJIAVJN3gtO3k4xy+7i2Xjtwh8CfPcH08WYjZpe9xzveDaqGw9fVNCpkYu6M525wKFVkLmyi7ku+DxCAP1lyMA== +jest-haste-map@^26.5.2, jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" jest-regex-util "^26.0.0" - jest-serializer "^26.5.0" - jest-util "^26.5.2" - jest-worker "^26.5.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.4.2.tgz#18a9d5bec30904267ac5e9797570932aec1e2257" - integrity sha512-z7H4EpCldHN1J8fNgsja58QftxBSL+JcwZmaXIvV9WKIM+x49F4GLHu/+BQh2kzRKHAgaN/E82od+8rTOBPyPA== +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.3.0" - "@jest/source-map" "^26.3.0" - "@jest/test-result" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.4.2" + expect "^26.6.2" is-generator-fn "^2.0.0" - jest-each "^26.4.2" - jest-matcher-utils "^26.4.2" - jest-message-util "^26.3.0" - jest-runtime "^26.4.2" - jest-snapshot "^26.4.2" - jest-util "^26.3.0" - pretty-format "^26.4.2" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" throat "^5.0.0" -jest-leak-detector@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.4.2.tgz#c73e2fa8757bf905f6f66fb9e0070b70fa0f573f" - integrity sha512-akzGcxwxtE+9ZJZRW+M2o+nTNnmQZxrHJxX/HjgDaU5+PLmY1qnQPnMjgADPGCRPhB+Yawe1iij0REe+k/aHoA== +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== dependencies: jest-get-type "^26.3.0" - pretty-format "^26.4.2" + pretty-format "^26.6.2" jest-matcher-utils@^24.9.0: version "24.9.0" @@ -17620,15 +17606,15 @@ jest-matcher-utils@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-matcher-utils@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.4.2.tgz#fa81f3693f7cb67e5fc1537317525ef3b85f4b06" - integrity sha512-KcbNqWfWUG24R7tu9WcAOKKdiXiXCbMvQYT6iodZ9k1f7065k0keUOW6XpJMMvah+hTfqkhJhRXmA3r3zMAg0Q== +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== dependencies: chalk "^4.0.0" - jest-diff "^26.4.2" + jest-diff "^26.6.2" jest-get-type "^26.3.0" - pretty-format "^26.4.2" + pretty-format "^26.6.2" jest-message-util@^24.9.0: version "24.9.0" @@ -17644,17 +17630,18 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" -jest-message-util@^26.3.0, jest-message-util@^26.5.2: - version "26.5.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.5.2.tgz#6c4c4c46dcfbabb47cd1ba2f6351559729bc11bb" - integrity sha512-Ocp9UYZ5Jl15C5PNsoDiGEk14A4NG0zZKknpWdZGoMzJuGAkVt10e97tnEVMYpk7LnQHZOfuK2j/izLBMcuCZw== +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.4" micromatch "^4.0.2" + pretty-format "^26.6.2" slash "^3.0.0" stack-utils "^2.0.2" @@ -17665,12 +17652,12 @@ jest-mock@^24.0.0, jest-mock@^24.9.0: dependencies: "@jest/types" "^24.9.0" -jest-mock@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.3.0.tgz#ee62207c3c5ebe5f35b760e1267fee19a1cfdeba" - integrity sha512-PeaRrg8Dc6mnS35gOo/CbZovoDPKAeB1FICZiuagAgGvbWdNNyjQjkOaGUa/3N3JtpQ/Mh9P4A2D4Fv51NnP8Q== +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" "@types/node" "*" jest-pnp-resolver@^1.2.1, jest-pnp-resolver@^1.2.2: @@ -17693,14 +17680,14 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.4.2.tgz#739bdb027c14befb2fe5aabbd03f7bab355f1dc5" - integrity sha512-ADHaOwqEcVc71uTfySzSowA/RdxUpCxhxa2FNLiin9vWLB1uLPad3we+JSSROq5+SrL9iYPdZZF8bdKM7XABTQ== +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" jest-regex-util "^26.0.0" - jest-snapshot "^26.4.2" + jest-snapshot "^26.6.2" jest-resolve@^24.9.0: version "24.9.0" @@ -17713,82 +17700,83 @@ jest-resolve@^24.9.0: jest-pnp-resolver "^1.2.1" realpath-native "^1.1.0" -jest-resolve@^26.4.0, jest-resolve@^26.5.2: - version "26.5.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.5.2.tgz#0d719144f61944a428657b755a0e5c6af4fc8602" - integrity sha512-XsPxojXGRA0CoDD7Vis59ucz2p3cQFU5C+19tz3tLEAlhYKkK77IL0cjYjikY9wXnOaBeEdm1rOgSJjbZWpcZg== +jest-resolve@^26.5.2, jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" graceful-fs "^4.2.4" jest-pnp-resolver "^1.2.2" - jest-util "^26.5.2" + jest-util "^26.6.2" read-pkg-up "^7.0.1" - resolve "^1.17.0" + resolve "^1.18.1" slash "^3.0.0" -jest-runner@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.4.2.tgz#c3ec5482c8edd31973bd3935df5a449a45b5b853" - integrity sha512-FgjDHeVknDjw1gRAYaoUoShe1K3XUuFMkIaXbdhEys+1O4bEJS8Avmn4lBwoMfL8O5oFTdWYKcf3tEJyyYyk8g== +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== dependencies: - "@jest/console" "^26.3.0" - "@jest/environment" "^26.3.0" - "@jest/test-result" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" emittery "^0.7.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.4.2" + jest-config "^26.6.3" jest-docblock "^26.0.0" - jest-haste-map "^26.3.0" - jest-leak-detector "^26.4.2" - jest-message-util "^26.3.0" - jest-resolve "^26.4.0" - jest-runtime "^26.4.2" - jest-util "^26.3.0" - jest-worker "^26.3.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.4.2.tgz#94ce17890353c92e4206580c73a8f0c024c33c42" - integrity sha512-4Pe7Uk5a80FnbHwSOk7ojNCJvz3Ks2CNQWT5Z7MJo4tX0jb3V/LThKvD9tKPNVNyeMH98J/nzGlcwc00R2dSHQ== - dependencies: - "@jest/console" "^26.3.0" - "@jest/environment" "^26.3.0" - "@jest/fake-timers" "^26.3.0" - "@jest/globals" "^26.4.2" - "@jest/source-map" "^26.3.0" - "@jest/test-result" "^26.3.0" - "@jest/transform" "^26.3.0" - "@jest/types" "^26.3.0" +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/yargs" "^15.0.0" chalk "^4.0.0" + cjs-module-lexer "^0.6.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.4.2" - jest-haste-map "^26.3.0" - jest-message-util "^26.3.0" - jest-mock "^26.3.0" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" jest-regex-util "^26.0.0" - jest-resolve "^26.4.0" - jest-snapshot "^26.4.2" - jest-util "^26.3.0" - jest-validate "^26.4.2" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^15.3.1" + yargs "^15.4.1" -jest-serializer@^26.5.0: - version "26.5.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.5.0.tgz#f5425cc4c5f6b4b355f854b5f0f23ec6b962bc13" - integrity sha512-+h3Gf5CDRlSLdgTv7y0vPIAoLgX/SI7T4v6hy+TEXMgYbv+ztzbg5PSN6mUXAT/hXYHvZRWm+MaObVfqkhCGxA== +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== dependencies: "@types/node" "*" graceful-fs "^4.2.4" @@ -17820,25 +17808,26 @@ jest-snapshot@^24.1.0: pretty-format "^24.9.0" semver "^6.2.0" -jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.4.2.tgz#87d3ac2f2bd87ea8003602fbebd8fcb9e94104f6" - integrity sha512-N6Uub8FccKlf5SBFnL2Ri/xofbaA68Cc3MGjP/NuwgnsvWh+9hLIR/DhrxbSiKXMY9vUW5dI6EW1eHaDHqe9sg== +jest-snapshot@^26.3.0, jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.0.0" chalk "^4.0.0" - expect "^26.4.2" + expect "^26.6.2" graceful-fs "^4.2.4" - jest-diff "^26.4.2" + jest-diff "^26.6.2" jest-get-type "^26.3.0" - jest-haste-map "^26.3.0" - jest-matcher-utils "^26.4.2" - jest-message-util "^26.3.0" - jest-resolve "^26.4.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" natural-compare "^1.4.0" - pretty-format "^26.4.2" + pretty-format "^26.6.2" semver "^7.3.2" jest-specific-snapshot@2.0.0: @@ -17880,41 +17869,41 @@ jest-util@^24.0.0: slash "^2.0.0" source-map "^0.6.0" -jest-util@^26.3.0, jest-util@^26.5.2: - version "26.5.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.5.2.tgz#8403f75677902cc52a1b2140f568e91f8ed4f4d7" - integrity sha512-WTL675bK+GSSAYgS8z9FWdCT2nccO1yTIplNLPlP0OD8tUk/H5IrWKMMRudIQQ0qp8bb4k+1Qa8CxGKq9qnYdg== +jest-util@^26.5.2, jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" graceful-fs "^4.2.4" is-ci "^2.0.0" micromatch "^4.0.2" -jest-validate@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.4.2.tgz#e871b0dfe97747133014dcf6445ee8018398f39c" - integrity sha512-blft+xDX7XXghfhY0mrsBCYhX365n8K5wNDC4XAcNKqqjEzsRUSXP44m6PL0QJEW2crxQFLLztVnJ4j7oPlQrQ== +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" camelcase "^6.0.0" chalk "^4.0.0" jest-get-type "^26.3.0" leven "^3.1.0" - pretty-format "^26.4.2" + pretty-format "^26.6.2" -jest-watcher@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.3.0.tgz#f8ef3068ddb8af160ef868400318dc4a898eed08" - integrity sha512-XnLdKmyCGJ3VoF6G/p5ohbJ04q/vv5aH9ENI+i6BL0uu9WWB6Z7Z2lhQQk0d2AVZcRGp1yW+/TsoToMhBFPRdQ== +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== dependencies: - "@jest/test-result" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.3.0" + jest-util "^26.6.2" string-length "^4.0.1" jest-when@^2.7.2: @@ -17933,23 +17922,23 @@ jest-worker@^25.4.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.2.1, jest-worker@^26.3.0, jest-worker@^26.5.0: - version "26.5.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" - integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== +jest-worker@^26.2.1, jest-worker@^26.5.0, jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.4.2.tgz#7e8bfb348ec33f5459adeaffc1a25d5752d9d312" - integrity sha512-LLCjPrUh98Ik8CzW8LLVnSCfLaiY+wbK53U7VxnFSX7Q+kWC4noVeDvGWIFw0Amfq1lq2VfGm7YHWSLBV62MJw== +jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== dependencies: - "@jest/core" "^26.4.2" + "@jest/core" "^26.6.3" import-local "^3.0.2" - jest-cli "^26.4.2" + jest-cli "^26.6.3" jimp@^0.14.0: version "0.14.0" @@ -18115,7 +18104,7 @@ jsdom@13.1.0, jsdom@^13.0.0: ws "^6.1.2" xml-name-validator "^3.0.0" -jsdom@^16.2.2: +jsdom@^16.4.0: version "16.4.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== @@ -18798,16 +18787,28 @@ livereload-js@^2.3.0: resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== -lmdb-store@^0.6.10: - version "0.6.10" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.6.10.tgz#db8efde6e052aabd17ebc63c8a913e1f31694129" - integrity sha512-ZLvp3qbBQ5VlBmaWa4EUAPyYEZ8qdUHsW69HmxkDi84pFQ37WMxYhFaF/7PQkdtxS/vyiKkZigd9TFgHjek1Nw== +lmdb-store-0.9@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/lmdb-store-0.9/-/lmdb-store-0.9-0.7.3.tgz#c2cb27dfa916ab966cceed692c67e4236813104a" + integrity sha512-t8iCnN6T3NZPFelPmjYIjCg+nhGbOdc0xcHCG40v01AWRTN49OINSt2k/u+16/2/HrI+b6Ssb8WByXUhbyHz6w== dependencies: fs-extra "^9.0.1" - msgpackr "^0.5.0" + msgpackr "^0.5.3" nan "^2.14.1" node-gyp-build "^4.2.3" - weak-lru-cache "^0.2.0" + weak-lru-cache "^0.3.9" + +lmdb-store@^0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.8.15.tgz#4efb0341c2df505dd6f3a7f26f834f0a142a80a2" + integrity sha512-4Q0WZh2FmcJC6esZRUWMfkCmNiz0WU9cOgrxt97ZMTnVfHyOdZhtrt0oOF5EQPfetxxJf/BorKY28aX92R6G6g== + dependencies: + fs-extra "^9.0.1" + lmdb-store-0.9 "0.7.3" + msgpackr "^0.5.4" + nan "^2.14.1" + node-gyp-build "^4.2.3" + weak-lru-cache "^0.3.9" load-bmfont@^1.3.1, load-bmfont@^1.4.0: version "1.4.0" @@ -19741,7 +19742,7 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.3.0, meow@^3.7.0: +meow@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= @@ -20323,20 +20324,20 @@ ms@2.1.1, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -msgpackr-extract@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.4.tgz#8ee5e73d1135340e564c498e8c593134365eb060" - integrity sha512-d3+qwTJzgqqsq2L2sQuH0SoO4StvpUhMqMAKy6tMimn7XdBaRtDlquFzRJsp0iMGt2hnU4UOqD8Tz9mb0KglTA== +msgpackr-extract@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.5.tgz#0f206da058bd3dad0f8605d324de001a8f4de967" + integrity sha512-zHhstybu+m/j3H6CVBMcILVIzATK6dWRGtlePJjsnSAj8kLT5joMa9i0v21Uc80BPNDcwFsnG/dz2318tfI81w== dependencies: nan "^2.14.1" node-gyp-build "^4.2.3" -msgpackr@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.5.1.tgz#7eecbf342645b7718dd2e3386894368d06732b3f" - integrity sha512-nK2uJl67Q5KU3MWkYBUlYynqKS1UUzJ5M1h6TQejuJtJzD3hW2Suv2T1pf01E9lUEr93xaLokf/xC+jwBShMPQ== +msgpackr@^0.5.3, msgpackr@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.5.4.tgz#c21c03d5e132d2e54d0b9ced02a75b1f48413380" + integrity sha512-ILEWtIWwd5ESWHKoVjJ4GP7JWkpuAUJ20qi2j2qEC6twecBmK4E6YG3QW847OpmvdAhMJGq2LoDJRn/kNERTeQ== optionalDependencies: - msgpackr-extract "^0.3.4" + msgpackr-extract "^0.3.5" multicast-dns-service-types@^1.1.0: version "1.1.0" @@ -22461,15 +22462,15 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.4.0, pretty-format@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" - integrity sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA== +pretty-format@^26.4.0, pretty-format@^26.4.2, pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.6.2" ansi-regex "^5.0.0" ansi-styles "^4.0.0" - react-is "^16.12.0" + react-is "^17.0.1" pretty-hrtime@^1.0.0, pretty-hrtime@^1.0.3: version "1.0.3" @@ -23293,6 +23294,11 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + react-is@~16.3.0: version "16.3.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" @@ -24646,11 +24652,12 @@ resolve@1.8.1: dependencies: path-parse "^1.0.5" -resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== +resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== dependencies: + is-core-module "^2.1.0" path-parse "^1.0.6" resolve@~1.10.1: @@ -28409,19 +28416,19 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== -v8-to-istanbul@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz#0608f5b49a481458625edb058488607f25498ba5" - integrity sha512-mbDNjuDajqYe3TXFk5qxcQy8L1msXNE37WTlLoqqpBfRsimbNcrlhQlDPntmECEcUvdC+AQ8CyMMf6EUx1r74Q== +v8-to-istanbul@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-6.0.1.tgz#7ef0e32faa10f841fe4c1b0f8de96ed067c0be1e" + integrity sha512-PzM1WlqquhBvsV+Gco6WSFeg1AGdD53ccMRkFeyHRE/KRZaVacPOmQYP3EeVgDBtKD2BJ8kgynBQ5OtKiHCH+w== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" source-map "^0.7.3" -v8-to-istanbul@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-6.0.1.tgz#7ef0e32faa10f841fe4c1b0f8de96ed067c0be1e" - integrity sha512-PzM1WlqquhBvsV+Gco6WSFeg1AGdD53ccMRkFeyHRE/KRZaVacPOmQYP3EeVgDBtKD2BJ8kgynBQ5OtKiHCH+w== +v8-to-istanbul@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" + integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -29129,10 +29136,10 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -weak-lru-cache@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-0.2.0.tgz#447379ccff6dfda1b7a9566c9ef168260be859d1" - integrity sha512-M1l5CzKvM7maa7tCbtL0NW6sOnp8gqup853+9Aq7GL0XNWKNnFOkeE3v3Z5X2IeMzedPwQyPbi4RlFvD6rxs7A== +weak-lru-cache@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-0.3.9.tgz#9e56920d4115e8542625d8ef8cc278cbd97f7624" + integrity sha512-WqAu3wzbHQvjSi/vgYhidZkf2p7L3Z8iDEIHnqvE31EQQa7Vh7PDOphrRJ1oxlW8JIjgr2HvMcRe9Q1GhW2NPw== web-namespaces@^1.0.0: version "1.1.4"