diff --git a/.ci/teamcity/bootstrap.sh b/.ci/teamcity/bootstrap.sh new file mode 100755 index 00000000000000..adb884ca064ba5 --- /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 00000000000000..3f7daef6d04731 --- /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 00000000000000..821647a39441cf --- /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 00000000000000..66578a4970fec8 --- /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 00000000000000..f269816cf6b95b --- /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 00000000000000..2baca870746301 --- /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 00000000000000..6413584d2057d0 --- /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 00000000000000..21ee68e5ade700 --- /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 00000000000000..8afc195fee5557 --- /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 00000000000000..da8ae3373d976e --- /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 00000000000000..4c2ddf5ce8612c --- /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 00000000000000..8571e0bbceb13a --- /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 00000000000000..2953661eca1fd9 --- /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 00000000000000..0df9329167ff65 --- /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 00000000000000..2868db9d067b84 --- /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 00000000000000..af90e24ef5fe82 --- /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 00000000000000..76c553b4f8fa24 --- /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 00000000000000..26c2c563210ed3 --- /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 00000000000000..5922a72bd5e85a --- /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 00000000000000..f5b57ce3b06eb9 --- /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 00000000000000..46048f6c82d52b --- /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 00000000000000..f983713e80f4d5 --- /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 00000000000000..63e54987f788f7 --- /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 00000000000000..bcc79e696d7839 --- /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 00000000000000..09693d7ebdc57b --- /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 00000000000000..3ef14b16633552 --- /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 00000000000000..28e3c9247f1d40 --- /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 00000000000000..3b2fb7ea912b76 --- /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 00000000000000..5e2a6c17c00527 --- /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 00000000000000..41ff549945c0b4 --- /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 00000000000000..6b381530d9bb7f --- /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 00000000000000..f662d36247a2fd --- /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 00000000000000..b805a2aa6fe62c --- /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 00000000000000..ea6c43c39e3978 --- /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 00000000000000..21ee68e5ade700 --- /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 00000000000000..3feaa821424e14 --- /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 00000000000000..39f79f94744c70 --- /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 00000000000000..e3829c961fac8a --- /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 00000000000000..fe1afdf04c54c1 --- /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 5b43f9883a2c1f..93d49dc18d417a 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/.teamcity/.editorconfig b/.teamcity/.editorconfig new file mode 100644 index 00000000000000..db789a8c72de1a --- /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 00000000000000..c8f78f4575965f 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 00000000000000..77c0bc5bc4cd31 --- /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 00000000000000..5fa068d0a92e03 --- /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 00000000000000..ec1b1c6eb94ef6 --- /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 00000000000000..120b333d43e724 --- /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 00000000000000..ae316960acf89f --- /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 00000000000000..1228ea4d94f4c9 --- /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 00000000000000..7f19304428d7e1 --- /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 00000000000000..605a22f0129763 --- /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 00000000000000..0b3b3b013b5ec3 --- /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 00000000000000..d3eb697981ce7c --- /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 00000000000000..f0a9c60cf3e450 --- /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 00000000000000..f4683e6cf0c1a0 --- /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 00000000000000..7dbe9cd0ba84c4 --- /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 00000000000000..6f1d45598c92eb --- /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 00000000000000..2429967d249392 --- /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 00000000000000..d8124bd8521c0a --- /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 00000000000000..61505d4757faaa --- /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 00000000000000..1c3b85257c28a2 --- /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 00000000000000..d0c849ff5f9964 --- /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 00000000000000..9303439d49f307 --- /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 00000000000000..f80a97873b2461 --- /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 00000000000000..c778814af536c4 --- /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 00000000000000..8e4a7acd77b768 --- /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 00000000000000..50fd73c17ba426 --- /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 00000000000000..1c188cd4c175fc --- /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 00000000000000..931cca2554a24f --- /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 00000000000000..2db8314fa44fc5 --- /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 00000000000000..d8189fd3589660 --- /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 00000000000000..7fbf863820e4c8 --- /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 00000000000000..d1b5898d1a5f5e --- /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 00000000000000..d595840c879e67 --- /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 00000000000000..04217a4e99b1c1 --- /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 00000000000000..9ec1360dcb1d76 --- /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 00000000000000..1fdb1e366e83fb --- /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 00000000000000..1958d39183bae7 --- /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 00000000000000..a5aa47d5cae487 --- /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 00000000000000..20c30eedf5b91d --- /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 00000000000000..762218b72ab107 --- /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 00000000000000..117c30ddb86e31 --- /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 00000000000000..ab7120b854446f --- /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 00000000000000..d847a1565e6e06 --- /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 00000000000000..677effec5be658 --- /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.indexpattern.getfieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md index 0c1fbe7d0d1b65..a0b54c6de50c9d 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 @@ -8,8 +8,6 @@ ```typescript getFieldAttrs: () => { - [x: string]: { - customLabel: string; - }; + [x: string]: FieldAttrSet; }; ``` 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 3383116f404b20..6bd2cbc24283f9 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]: {
customLabel: string;
};
} | | +| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: FieldAttrSet;
} | | | [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.querystringinputprops.disablelanguageswitcher.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.disablelanguageswitcher.md new file mode 100644 index 00000000000000..c11edd95a891b4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.disablelanguageswitcher.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [disableLanguageSwitcher](./kibana-plugin-plugins-data-public.querystringinputprops.disablelanguageswitcher.md) + +## QueryStringInputProps.disableLanguageSwitcher property + +Signature: + +```typescript +disableLanguageSwitcher?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.icontype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.icontype.md new file mode 100644 index 00000000000000..4b1c5b84557e7b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.icontype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [iconType](./kibana-plugin-plugins-data-public.querystringinputprops.icontype.md) + +## QueryStringInputProps.iconType property + +Signature: + +```typescript +iconType?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md index d503980da7947f..90c604d131800f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md @@ -18,6 +18,8 @@ export interface QueryStringInputProps | [className](./kibana-plugin-plugins-data-public.querystringinputprops.classname.md) | string | | | [dataTestSubj](./kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md) | string | | | [disableAutoFocus](./kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md) | boolean | | +| [disableLanguageSwitcher](./kibana-plugin-plugins-data-public.querystringinputprops.disablelanguageswitcher.md) | boolean | | +| [iconType](./kibana-plugin-plugins-data-public.querystringinputprops.icontype.md) | string | | | [indexPatterns](./kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md) | Array<IIndexPattern | string> | | | [isInvalid](./kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md) | boolean | | | [languageSwitcherPopoverAnchorPosition](./kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md) | PopoverAnchorPosition | | 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 b1e38258353c35..f98acd766ac339 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 @@ -8,8 +8,6 @@ ```typescript getFieldAttrs: () => { - [x: string]: { - customLabel: string; - }; + [x: string]: FieldAttrSet; }; ``` 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 5103af52f1b435..e5e2dfd0999db2 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]: {
customLabel: string;
};
} | | +| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: FieldAttrSet;
} | | | [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/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index dbac6997ff4333..6244a43b54f724 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/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index ca788020d92862..1b9896d7dea565 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/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index aab191111e92e8..8eb54d68d48cf9 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -127,9 +127,10 @@ export class BookEmbeddableFactoryDefinition private async unwrapMethod(savedObjectId: string): Promise { const { savedObjectsClient } = await this.getStartServices(); - const savedObject: SimpleSavedObject = await savedObjectsClient.get< - BookSavedObjectAttributes - >(this.type, savedObjectId); + const savedObject: SimpleSavedObject = await savedObjectsClient.get( + this.type, + savedObjectId + ); return { ...savedObject.attributes }; } @@ -163,9 +164,9 @@ export class BookEmbeddableFactoryDefinition private async getAttributeService() { if (!this.attributeService) { - this.attributeService = (await this.getStartServices()).getAttributeService< - BookSavedObjectAttributes - >(this.type, { + this.attributeService = ( + await this.getStartServices() + ).getAttributeService(this.type, { saveMethod: this.saveMethod.bind(this), unwrapMethod: this.unwrapMethod.bind(this), checkForDuplicateTitle: this.checkForDuplicateTitleMethod.bind(this), diff --git a/package.json b/package.json index 4362f2d111b646..8e94e5277b8e3e 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "**/load-grunt-config/lodash": "^4.17.20", "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", + "**/prismjs": "1.22.0", "**/request": "^2.88.2", "**/trim": "0.0.3", "**/typescript": "4.0.2" @@ -131,6 +132,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 +421,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", @@ -510,7 +511,7 @@ "@types/pdfmake": "^0.1.15", "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.4.0", - "@types/prettier": "^2.0.2", + "@types/prettier": "^2.1.5", "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", @@ -602,7 +603,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", @@ -627,7 +627,7 @@ "enzyme-adapter-utils": "^1.13.0", "enzyme-to-json": "^3.4.4", "eslint": "^6.8.0", - "eslint-config-prettier": "^6.11.0", + "eslint-config-prettier": "^6.15.0", "eslint-import-resolver-node": "0.3.2", "eslint-import-resolver-webpack": "0.11.1", "eslint-module-utils": "2.5.0", @@ -763,7 +763,7 @@ "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "postcss-prefix-selector": "^1.7.2", - "prettier": "^2.1.1", + "prettier": "^2.2.0", "pretty-ms": "5.0.0", "proxyquire": "1.8.0", "querystring": "^0.2.0", diff --git a/packages/kbn-legacy-logging/README.md b/packages/kbn-legacy-logging/README.md new file mode 100644 index 00000000000000..4c5989fc892dc5 --- /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 00000000000000..9311b3e2a77b37 --- /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 267dc9a334de88..cf49177e50b7be 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 00000000000000..0fa5f65abf8617 --- /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 2f6c34e0fc5d6b..9b1ba87c250dcc 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 690c9c0bfe21d5..45e4bda0b007c0 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 00000000000000..296c255a75185c --- /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 6edda8c4be9076..e357c2420c1788 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 ec7296d21672b2..f762daf01e5fa9 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 bfceb78b24504c..7961fda7912ccc 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 842325865cce22..0ed233228c1fd9 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 f696e6f9b0002c..b4217c37b960e3 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 prefix = process.env.isDevCliChild ? `${type('server')} ` : ''; -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 492d1ffc8d167f..32da6432cc443a 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 2298d83aa28571..2d559dc1ef55cb 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 4afb00b568844b..8ecaf348bac04c 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 73e03a154907ac..8b7c2f8f87c590 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 90% rename from src/legacy/server/logging/rotate/index.ts rename to packages/kbn-legacy-logging/src/rotate/index.ts index cc7186c06cf3ff..9a83c625b9431b 100644 --- a/src/legacy/server/logging/rotate/index.ts +++ b/packages/kbn-legacy-logging/src/rotate/index.ts @@ -19,19 +19,19 @@ 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; } // 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 8f67b47f6949e0..1f6407d2cca30d 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 a82b7baa83a329..11d3f497916dcf 100644 --- a/src/legacy/server/logging/rotate/log_rotator.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts @@ -26,7 +26,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); @@ -36,7 +36,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; @@ -51,19 +51,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; } @@ -127,7 +127,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)); @@ -151,7 +154,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 00000000000000..5f0e4fe89422b8 --- /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 00000000000000..103e81249a136d --- /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 00000000000000..f13c869b563a29 --- /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 00000000000000..0f37a13f8a478b --- /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 e007157e9488b6..bfcc7b1c908d4a 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 63e5ab4c62f298..8fd7eac57fc32e 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 00000000000000..5841e7b6082843 --- /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 00000000000000..8fd202a2dce8ba --- /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 00000000000000..3f82ebc9cdbae3 --- /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-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 4a85eca206c965..f899a5b44ab6c1 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 5bbc72fe04e86e..910c9ad2467008 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 1413d054984594..30ec6ab939560a 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 93616ce78a04a5..9010e324bb392b 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/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts index 6004c48521c6d5..166ecb2363b450 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts @@ -196,6 +196,7 @@ function getSnapshotState(file: string, test: Test, updateSnapshots: boolean) { path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')), { updateSnapshot: updateSnapshots ? 'all' : 'new', + // @ts-expect-error getPrettier: () => prettier, getBabelTraverse: () => babelTraverse, } 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 407ab37123d5d4..605ad38efbc963 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 84d488bd8b5a10..de28fceb967e29 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/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index e527fdb9159706..7be3a31624df20 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -57,7 +57,9 @@ let coreContext: CoreContext; const logger = loggingSystemMock.create(); let mockClusterClientInstance: ReturnType; -let mockLegacyClusterClientInstance: ReturnType; +let mockLegacyClusterClientInstance: ReturnType< + typeof elasticsearchServiceMock.createLegacyCustomClusterClient +>; beforeEach(() => { env = Env.createDefault(REPO_ROOT, getEnvOptions()); 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 697e5bc37d6027..c4dca1b84f4eb8 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 67337c7d676297..286448231d23f1 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 afe58ddff92aa2..2fca2f35cb0323 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/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts index 1c4e55566b61b3..50a71913934720 100644 --- a/src/core/server/saved_objects/migrations/mocks.ts +++ b/src/core/server/saved_objects/migrations/mocks.ts @@ -20,9 +20,7 @@ import { SavedObjectMigrationContext } from './types'; import { SavedObjectsMigrationLogger } from './core'; -export const createSavedObjectsMigrationLoggerMock = (): jest.Mocked< - SavedObjectsMigrationLogger -> => { +export const createSavedObjectsMigrationLoggerMock = (): jest.Mocked => { const mock = { debug: jest.fn(), info: jest.fn(), diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index d6b30889eba5f8..8e4c73137033dd 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -217,9 +217,8 @@ describe('SavedObjectsService', () => { await soService.setup(setupDeps); soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); - ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< - NodesVersionCompatibility - >).next({ + ((setupDeps.elasticsearch + .esNodesCompatibility$ as any) as BehaviorSubject).next({ isCompatible: true, incompatibleNodes: [], warningNodes: [], diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index ea881805e1ae6b..0ba874c02e8ed5 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -66,9 +66,7 @@ describe('SavedObjectsRepository#createRepository', () => { }); const migrator = mockKibanaMigrator.create({ types: typeRegistry.getAllTypes() }); - const RepositoryConstructor = (SavedObjectsRepository as unknown) as jest.Mock< - SavedObjectsRepository - >; + const RepositoryConstructor = (SavedObjectsRepository as unknown) as jest.Mock; beforeEach(() => { RepositoryConstructor.mockClear(); diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 654c3f9948a18a..93d7218b11c281 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 d859c7e45fa200..8448d20aa2fc88 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 39df3990ff2ff2..a9b5eec45a75bd 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 eeef62e2bcb7dc..85d75b4e18772c 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -18,11 +18,11 @@ */ import { constant, once, compact, flatten } from 'lodash'; +import { reconfigureLogging } from '@kbn/legacy-logging'; // 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'; @@ -153,13 +153,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 5182de0b7f6130..cb252ba37dc4ef 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 0123459bd25d2e..2a9b82afefc984 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/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 393b7eee346f5f..84b12c97f1856b 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -368,20 +368,21 @@ export class LegacyCoreEditor implements CoreEditor { // disable standard context based autocompletion. // @ts-ignore - ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function ( - require: any, - exports: any - ) { - exports.getCompletions = function ( - innerEditor: any, - session: any, - pos: any, - prefix: any, - callback: any - ) { - callback(null, []); - }; - }); + ace.define( + 'ace/autocomplete/text_completer', + ['require', 'exports', 'module'], + function (require: any, exports: any) { + exports.getCompletions = function ( + innerEditor: any, + session: any, + pos: any, + prefix: any, + callback: any + ) { + callback(null, []); + }; + } + ); const langTools = ace.acequire('ace/ext/language_tools'); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index 0b443ef400d6fc..89880528943e52 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -84,90 +84,93 @@ describe('Integration', () => { changeListener: function () {}, }; // mimic auto complete - senseEditor.autocomplete._test.getCompletions(senseEditor, null, cursor, '', function ( - err, - terms - ) { - if (testToRun.assertThrows) { - done(); - return; - } + senseEditor.autocomplete._test.getCompletions( + senseEditor, + null, + cursor, + '', + function (err, terms) { + if (testToRun.assertThrows) { + done(); + return; + } - if (err) { - throw err; - } + if (err) { + throw err; + } - if (testToRun.no_context) { - expect(!terms || terms.length === 0).toBeTruthy(); - } else { - expect(terms).not.toBeNull(); - expect(terms.length).toBeGreaterThan(0); - } + if (testToRun.no_context) { + expect(!terms || terms.length === 0).toBeTruthy(); + } else { + expect(terms).not.toBeNull(); + expect(terms.length).toBeGreaterThan(0); + } - if (!terms || terms.length === 0) { - done(); - return; - } + if (!terms || terms.length === 0) { + done(); + return; + } - if (testToRun.autoCompleteSet) { - const expectedTerms = _.map(testToRun.autoCompleteSet, function (t) { - if (typeof t !== 'object') { - t = { name: t }; - } - return t; - }); - if (terms.length !== expectedTerms.length) { - expect(_.map(terms, 'name')).toEqual(_.map(expectedTerms, 'name')); - } else { - const filteredActualTerms = _.map(terms, function (actualTerm, i) { - const expectedTerm = expectedTerms[i]; - const filteredTerm = {}; - _.each(expectedTerm, function (v, p) { - filteredTerm[p] = actualTerm[p]; - }); - return filteredTerm; + if (testToRun.autoCompleteSet) { + const expectedTerms = _.map(testToRun.autoCompleteSet, function (t) { + if (typeof t !== 'object') { + t = { name: t }; + } + return t; }); - expect(filteredActualTerms).toEqual(expectedTerms); + if (terms.length !== expectedTerms.length) { + expect(_.map(terms, 'name')).toEqual(_.map(expectedTerms, 'name')); + } else { + const filteredActualTerms = _.map(terms, function (actualTerm, i) { + const expectedTerm = expectedTerms[i]; + const filteredTerm = {}; + _.each(expectedTerm, function (v, p) { + filteredTerm[p] = actualTerm[p]; + }); + return filteredTerm; + }); + expect(filteredActualTerms).toEqual(expectedTerms); + } } - } - const context = terms[0].context; - const { - cursor: { lineNumber, column }, - } = testToRun; - senseEditor.autocomplete._test.addReplacementInfoToContext( - context, - { lineNumber, column }, - terms[0].value - ); + const context = terms[0].context; + const { + cursor: { lineNumber, column }, + } = testToRun; + senseEditor.autocomplete._test.addReplacementInfoToContext( + context, + { lineNumber, column }, + terms[0].value + ); - function ac(prop, propTest) { - if (typeof testToRun[prop] !== 'undefined') { - if (propTest) { - propTest(context[prop], testToRun[prop], prop); - } else { - expect(context[prop]).toEqual(testToRun[prop]); + function ac(prop, propTest) { + if (typeof testToRun[prop] !== 'undefined') { + if (propTest) { + propTest(context[prop], testToRun[prop], prop); + } else { + expect(context[prop]).toEqual(testToRun[prop]); + } } } - } - function posCompare(actual, expected) { - expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); - expect(actual.column).toEqual(expected.column); - } + function posCompare(actual, expected) { + expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); + expect(actual.column).toEqual(expected.column); + } - function rangeCompare(actual, expected, name) { - posCompare(actual.start, expected.start, name + '.start'); - posCompare(actual.end, expected.end, name + '.end'); - } + function rangeCompare(actual, expected, name) { + posCompare(actual.start, expected.start, name + '.start'); + posCompare(actual.end, expected.end, name + '.end'); + } - ac('prefixToAdd'); - ac('suffixToAdd'); - ac('addTemplate'); - ac('textBoxPosition', posCompare); - ac('rangeToReplace', rangeCompare); - done(); - }); + ac('prefixToAdd'); + ac('suffixToAdd'); + ac('addTemplate'); + ac('textBoxPosition', posCompare); + ac('rangeToReplace', rangeCompare); + done(); + } + ); }); } diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 24bf736cfa2740..c47a4c2d21b117 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -179,9 +179,7 @@ export class DashboardPlugin core: CoreSetup, { share, uiActions, embeddable, home, urlForwarding, data, usageCollection }: SetupDependencies ): DashboardSetup { - this.dashboardFeatureFlagConfig = this.initializerContext.config.get< - DashboardFeatureFlagConfig - >(); + this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); 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 47ad5860801bc4..52672c71dcce4e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,7 +18,7 @@ */ import _, { each, reject } from 'lodash'; -import { FieldAttrs } from '../..'; +import { FieldAttrs, FieldAttrSet } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -135,8 +135,19 @@ export class IndexPattern implements IIndexPattern { const newFieldAttrs = { ...this.fieldAttrs }; this.fields.forEach((field) => { + const attrs: FieldAttrSet = {}; + let hasAttr = false; if (field.customLabel) { - newFieldAttrs[field.name] = { customLabel: field.customLabel }; + attrs.customLabel = field.customLabel; + hasAttr = true; + } + if (field.count) { + attrs.count = field.count; + hasAttr = true; + } + + if (hasAttr) { + newFieldAttrs[field.name] = attrs; } else { delete newFieldAttrs[field.name]; } @@ -298,7 +309,9 @@ export class IndexPattern implements IIndexPattern { timeFieldName: this.timeFieldName, intervalName: this.intervalName, sourceFilters: this.sourceFilters ? JSON.stringify(this.sourceFilters) : undefined, - fields: this.fields ? JSON.stringify(this.fields) : undefined, + fields: this.fields + ? JSON.stringify(this.fields.filter((field) => field.scripted)) + : undefined, fieldFormatMap, type: this.type, typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index bf227615f76a1a..2e9c27735a8d1f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -155,15 +155,11 @@ describe('IndexPatterns', () => { // Create a normal index patterns const pattern = await indexPatterns.get('foo'); - - expect(pattern.version).toBe('fooa'); indexPatterns.clearCache(); // Create the same one - we're going to handle concurrency const samePattern = await indexPatterns.get('foo'); - expect(samePattern.version).toBe('fooaa'); - // This will conflict because samePattern did a save (from refreshFields) // but the resave should work fine pattern.title = 'foo2'; 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 82c8cf4abc5ac3..e09246ae8cd3e7 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 @@ -197,22 +197,6 @@ export class IndexPatternsService { } }; - private isFieldRefreshRequired(specs?: IndexPatternFieldMap): boolean { - if (!specs) { - return true; - } - - return Object.values(specs).every((spec) => { - // See https://github.com/elastic/kibana/pull/8421 - const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec; - - // See https://github.com/elastic/kibana/pull/11969 - const hasDocValuesFlag = 'readFromDocValues' in spec; - - return !hasFieldCaps || !hasDocValuesFlag; - }); - } - /** * Get field list by providing { pattern } * @param options @@ -299,8 +283,8 @@ export class IndexPatternsService { values: { id, title }, }), }); + throw err; } - return fields; }; /** @@ -309,7 +293,11 @@ export class IndexPatternsService { */ fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => fields.reduce((collector, field) => { - collector[field.name] = { ...field, customLabel: fieldAttrs?.[field.name]?.customLabel }; + collector[field.name] = { + ...field, + customLabel: fieldAttrs?.[field.name]?.customLabel, + count: fieldAttrs?.[field.name]?.count, + }; return collector; }, {}); @@ -372,25 +360,20 @@ export class IndexPatternsService { ? JSON.parse(savedObject.attributes.fieldAttrs) : {}; - const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields); - let isSaveRequired = isFieldRefreshRequired; try { - spec.fields = isFieldRefreshRequired - ? await this.refreshFieldSpecMap( - spec.fields || {}, - id, - spec.title as string, - { - pattern: title as string, - metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), - type, - rollupIndex: typeMeta?.params?.rollupIndex, - }, - spec.fieldAttrs - ) - : spec.fields; + spec.fields = await this.refreshFieldSpecMap( + spec.fields || {}, + id, + spec.title as string, + { + pattern: title as string, + metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), + type, + rollupIndex: typeMeta?.params?.rollup_index, + }, + spec.fieldAttrs + ); } catch (err) { - isSaveRequired = false; if (err instanceof IndexPatternMissingIndices) { this.onNotification({ title: (err as any).message, @@ -412,23 +395,6 @@ export class IndexPatternsService { : {}; const indexPattern = await this.create(spec, true); - if (isSaveRequired) { - try { - this.updateSavedObject(indexPattern); - } catch (err) { - this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldSaveErrorTitle', { - defaultMessage: - 'Error saving after fetching fields for index pattern {title} (ID: {id})', - values: { - id: indexPattern.id, - title: indexPattern.title, - }, - }), - }); - } - } - indexPattern.resetOriginalSavedObjectBody(); return indexPattern; }; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 28b077f4bfdf31..8d9b29175162e8 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -52,7 +52,12 @@ export interface IndexPatternAttributes { } export interface FieldAttrs { - [key: string]: { customLabel: string }; + [key: string]: FieldAttrSet; +} + +export interface FieldAttrSet { + customLabel?: string; + count?: number; } export type OnNotification = (toastInputFields: ToastInputFields) => void; diff --git a/src/plugins/data/common/search/aggs/param_types/agg.ts b/src/plugins/data/common/search/aggs/param_types/agg.ts index e3f8c7c922170f..af9ee5fa628c4f 100644 --- a/src/plugins/data/common/search/aggs/param_types/agg.ts +++ b/src/plugins/data/common/search/aggs/param_types/agg.ts @@ -20,9 +20,9 @@ import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config'; import { BaseParamType } from './base'; -export class AggParamType extends BaseParamType< - TAggConfig -> { +export class AggParamType< + TAggConfig extends IAggConfig = IAggConfig +> extends BaseParamType { makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; allowedAggs: string[] = []; diff --git a/src/plugins/data/common/search/search_source/legacy/call_client.ts b/src/plugins/data/common/search/search_source/legacy/call_client.ts index cb6295dd701eee..eb6f298c8b220c 100644 --- a/src/plugins/data/common/search/search_source/legacy/call_client.ts +++ b/src/plugins/data/common/search/search_source/legacy/call_client.ts @@ -28,10 +28,9 @@ export function callClient( fetchHandlers: FetchHandlers ) { // Correlate the options with the request that they're associated with - const requestOptionEntries: Array<[ - SearchRequest, - ISearchOptions - ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); + const requestOptionEntries: Array< + [SearchRequest, ISearchOptions] + > = searchRequests.map((request, i) => [request, requestsOptions[i]]); const requestOptionsMap = new Map(requestOptionEntries); const requestResponseMap = new Map>>(); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index fc9b8d4839ea3c..5a707393b39f42 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1151,9 +1151,7 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getFieldAttrs: () => { - [x: string]: { - customLabel: string; - }; + [x: string]: FieldAttrSet; }; // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; @@ -1798,6 +1796,10 @@ export interface QueryStringInputProps { // (undocumented) disableAutoFocus?: boolean; // (undocumented) + disableLanguageSwitcher?: boolean; + // (undocumented) + iconType?: string; + // (undocumented) indexPatterns: Array; // (undocumented) isInvalid?: boolean; @@ -2362,6 +2364,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:135:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 1ff24c61954e7f..3e3982dd58e57a 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -19,11 +19,12 @@ z-index: $euiZContentMenu; resize: none !important; // When in the group, it will autosize height: $euiFormControlHeight; - // Unlike most inputs within layout control groups, the text area still needs a border. - // These adjusts help it sit above the control groups shadow to line up correctly. + // Unlike most inputs within layout control groups, the text area still needs a border + // for multi-line content. These adjusts help it sit above the control groups + // shadow to line up correctly. padding: $euiSizeS; padding-top: $euiSizeS + 3px; - transform: translateY(-1px) translateX(-1px); + box-shadow: 0 0 0 1px $euiFormBorderColor; &:not(:focus):not(:invalid) { @include euiYScrollWithShadows; @@ -40,6 +41,17 @@ overflow-x: auto; overflow-y: auto; white-space: normal; + box-shadow: 0 0 0 1px $euiFormBorderColor; + } + + @include euiFormControlWithIcon($isIconOptional: true); + ~ .euiFormControlLayoutIcons { + // By default form control layout icon is vertically centered, but our textarea + // can expand to be multi-line, so we position it with padding that matches + // the parent textarea padding + z-index: $euiZContentMenu + 1; + top: $euiSizeS + 3px; + bottom: unset; } } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 7d0ad4b3be097e..c26f1898a40845 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -29,7 +29,7 @@ import { mount } from 'enzyme'; import { waitFor } from '@testing-library/dom'; import { render } from '@testing-library/react'; -import { EuiTextArea } from '@elastic/eui'; +import { EuiTextArea, EuiIcon } from '@elastic/eui'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryStringInput } from './'; @@ -172,6 +172,30 @@ describe('QueryStringInput', () => { expect(mockCallback).toHaveBeenCalledWith({ query: '', language: 'lucene' }); }); + it('Should not show the language switcher when disabled', () => { + const component = mount( + wrapQueryStringInputInContext({ + query: luceneQuery, + onSubmit: noop, + indexPatterns: [stubIndexPatternWithFields], + disableLanguageSwitcher: true, + }) + ); + expect(component.find(QueryLanguageSwitcher).exists()).toBeFalsy(); + }); + + it('Should show an icon when an iconType is specified', () => { + const component = mount( + wrapQueryStringInputInContext({ + query: luceneQuery, + onSubmit: noop, + indexPatterns: [stubIndexPatternWithFields], + iconType: 'search', + }) + ); + expect(component.find(EuiIcon).exists()).toBeTruthy(); + }); + it('Should call onSubmit when the user hits enter inside the query bar', () => { const mockCallback = jest.fn(); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index bc9e2ed6a83ceb..a6d22ce3eb4734 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -31,6 +31,7 @@ import { EuiLink, htmlIdGenerator, EuiPortal, + EuiIcon, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -55,6 +56,7 @@ export interface QueryStringInputProps { persistedLog?: PersistedLog; bubbleSubmitEvent?: boolean; placeholder?: string; + disableLanguageSwitcher?: boolean; languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onBlur?: () => void; onChange?: (query: Query) => void; @@ -64,6 +66,7 @@ export interface QueryStringInputProps { size?: SuggestionsListSize; className?: string; isInvalid?: boolean; + iconType?: string; } interface Props extends QueryStringInputProps { @@ -608,13 +611,17 @@ export default class QueryStringInputUI extends Component { 'aria-owns': 'kbnTypeahead__items', }; const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; - const className = classNames( + const containerClassName = classNames( 'euiFormControlLayout euiFormControlLayout--group kbnQueryBar__wrap', this.props.className ); + const inputClassName = classNames( + 'kbnQueryBar__textarea', + this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null + ); return ( -
+
{this.props.prepend}
{ onClick={this.onClickInput} onBlur={this.onInputBlur} onFocus={this.handleOnFocus} - className="kbnQueryBar__textarea" + className={inputClassName} fullWidth rows={1} id={this.textareaId} @@ -678,6 +685,15 @@ export default class QueryStringInputUI extends Component { > {this.getQueryString()} + {this.props.iconType ? ( +
+
+ ) : null}
{
- - + {this.props.disableLanguageSwitcher ? null : ( + + )}
); } diff --git a/src/plugins/data/public/ui/typeahead/constants.ts b/src/plugins/data/public/ui/typeahead/constants.ts index 0e28891a14535e..8a3e5206d5d5e3 100644 --- a/src/plugins/data/public/ui/typeahead/constants.ts +++ b/src/plugins/data/public/ui/typeahead/constants.ts @@ -33,4 +33,4 @@ export const SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE = 250; * A distance in px to display suggestions list right under the query input without a gap * @public */ -export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 1; +export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 0; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 47e17c26398d37..94114288eb1f3f 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -611,9 +611,7 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getFieldAttrs: () => { - [x: string]: { - customLabel: string; - }; + [x: string]: FieldAttrSet; }; // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; @@ -1215,6 +1213,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:135:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/embeddable/common/mocks.ts b/src/plugins/embeddable/common/mocks.ts index a9ac144d1f2762..060c2003868c17 100644 --- a/src/plugins/embeddable/common/mocks.ts +++ b/src/plugins/embeddable/common/mocks.ts @@ -19,9 +19,7 @@ import { EmbeddablePersistableStateService } from './types'; -export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked< - EmbeddablePersistableStateService -> => { +export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked => { return { inject: jest.fn((state, references) => state), extract: jest.fn((state) => ({ state, references: [] })), diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index a5c5133dbc7028..292d7d3bf7a1e3 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -244,9 +244,10 @@ export abstract class Container< private createNewExplicitEmbeddableInput< TEmbeddableInput extends EmbeddableInput = EmbeddableInput, - TEmbeddable extends IEmbeddable = IEmbeddable< - TEmbeddableInput - > + TEmbeddable extends IEmbeddable< + TEmbeddableInput, + EmbeddableOutput + > = IEmbeddable >( id: string, factory: EmbeddableFactory, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index a086b447994eb9..260f06f27ea56e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -58,9 +58,9 @@ describe('', () => { expect(onFormData.mock.calls.length).toBe(1); - const [formDataInitial] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + const [formDataInitial] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(formDataInitial).toEqual({ name: 'Initial value', @@ -77,9 +77,9 @@ describe('', () => { expect(onFormData).toBeCalledTimes(2); - const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + const [formDataUpdated] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(formDataUpdated).toEqual({ name: 'updated value', @@ -135,9 +135,9 @@ describe('', () => { expect(onFormData.mock.calls.length).toBe(2); - const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + const [formDataUpdated] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(formDataUpdated).toEqual({ name: 'updated value', @@ -236,9 +236,9 @@ describe('', () => { expect(onFormData.mock.calls.length).toBe(2); // 2 as the form "isValid" change caused a re-render - const [formData] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + const [formData] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(formData).toEqual({ name: 'updated value', diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index 1a7f8832e4a4e5..e62bc483b539cd 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -54,9 +54,9 @@ describe('', () => { setup(); - const [{ data }] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + const [{ data }] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(data.internal).toEqual({ name: 'John', diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 9626aaa9b2459a..386d33a462849d 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -255,9 +255,9 @@ describe('useForm() hook', () => { setInputValue('usernameField', 'John'); }); - [{ data, isValid }] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + [{ data, isValid }] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(data.internal).toEqual({ user: { name: 'John' } }); // Transform name to uppercase as decalred in our serializer func @@ -305,9 +305,9 @@ describe('useForm() hook', () => { expect(onFormData.mock.calls.length).toBe(1); - const [{ data }] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< - OnUpdateHandler - >; + const [{ data }] = onFormData.mock.calls[ + onFormData.mock.calls.length - 1 + ] as Parameters; expect(data.internal).toEqual({ title: defaultValue.title, diff --git a/src/plugins/expressions/common/ast/build_function.ts b/src/plugins/expressions/common/ast/build_function.ts index 6cd16b2bc13540..a5260f9ba5800c 100644 --- a/src/plugins/expressions/common/ast/build_function.ts +++ b/src/plugins/expressions/common/ast/build_function.ts @@ -45,9 +45,9 @@ export type InferFunctionDefinition< : never; // Shortcut for inferring args from a function definition. -type FunctionArgs = InferFunctionDefinition< - FnDef ->['arguments']; +type FunctionArgs< + FnDef extends AnyExpressionFunctionDefinition +> = InferFunctionDefinition['arguments']; // Gets a list of possible arg names for a given function. type FunctionArgName = { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 50a469c338d73a..e53a6f7d58e1cc 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -359,9 +359,12 @@ export class Execution< // Check for missing required arguments. for (const argDef of Object.values(argDefs)) { - const { aliases, default: argDefault, name: argName, required } = argDef as ArgumentType< - any - > & { name: string }; + const { + aliases, + default: argDefault, + name: argName, + required, + } = argDef as ArgumentType & { name: string }; if ( typeof argDefault !== 'undefined' || !required || diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 401c71cba8f1dc..924f8d4830f733 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -68,9 +68,9 @@ export class ExpressionRenderHandler { this.onRenderError = onRenderError || defaultRenderErrorHandler; this.renderSubject = new Rx.BehaviorSubject(null as any | null); - this.render$ = this.renderSubject.asObservable().pipe(filter((_) => _ !== null)) as Observable< - any - >; + this.render$ = this.renderSubject + .asObservable() + .pipe(filter((_) => _ !== null)) as Observable; this.updateSubject = new Rx.Subject(); this.update$ = this.updateSubject.asObservable(); diff --git a/src/plugins/expressions/public/services.ts b/src/plugins/expressions/public/services.ts index e296566e661cda..3501c4971d1ec6 100644 --- a/src/plugins/expressions/public/services.ts +++ b/src/plugins/expressions/public/services.ts @@ -25,10 +25,12 @@ export const [getNotifications, setNotifications] = createGetterSetter('Renderers registry'); +export const [ + getRenderersRegistry, + setRenderersRegistry, +] = createGetterSetter('Renderers registry'); -export const [getExpressionsService, setExpressionsService] = createGetterSetter< - ExpressionsService ->('ExpressionsService'); +export const [ + getExpressionsService, + setExpressionsService, +] = createGetterSetter('ExpressionsService'); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index d8555d71d6ec01..ffb75801f39cfe 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -129,9 +129,9 @@ export class StepIndexPattern extends Component { - const { savedObjects } = await this.context.services.savedObjects.client.find< - IndexPatternAttributes - >({ + const { + savedObjects, + } = await this.context.services.savedObjects.client.find({ type: 'index-pattern', fields: ['title'], perPage: 10000, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 545eb86311dad4..423d6c3287cd88 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -44,9 +44,12 @@ const newFieldPlaceholder = i18n.translate( export const CreateEditField = withRouter( ({ indexPattern, mode, fieldName, history }: CreateEditFieldProps) => { - const { uiSettings, chrome, notifications, data } = useKibana< - IndexPatternManagmentContext - >().services; + const { + uiSettings, + chrome, + notifications, + data, + } = useKibana().services; const spec = mode === 'edit' && fieldName ? indexPattern.fields.getByName(fieldName)?.spec diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 67a20c428040f4..a8a4c87c561981 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -58,19 +58,6 @@ const mappingConflictHeader = i18n.translate( } ); -const confirmMessage = i18n.translate('indexPatternManagement.editIndexPattern.refreshLabel', { - defaultMessage: 'This action resets the popularity counter of each field.', -}); - -const confirmModalOptionsRefresh = { - confirmButtonText: i18n.translate('indexPatternManagement.editIndexPattern.refreshButton', { - defaultMessage: 'Refresh', - }), - title: i18n.translate('indexPatternManagement.editIndexPattern.refreshHeader', { - defaultMessage: 'Refresh field list?', - }), -}; - const confirmModalOptionsDelete = { confirmButtonText: i18n.translate('indexPatternManagement.editIndexPattern.deleteButton', { defaultMessage: 'Delete', @@ -118,16 +105,6 @@ export const EditIndexPattern = withRouter( setDefaultIndex(indexPattern.id || ''); }, [uiSettings, indexPattern.id]); - const refreshFields = () => { - overlays.openConfirm(confirmMessage, confirmModalOptionsRefresh).then(async (isConfirmed) => { - if (isConfirmed) { - await data.indexPatterns.refreshFields(indexPattern); - await data.indexPatterns.updateSavedObject(indexPattern); - setFields(indexPattern.getNonScriptedFields()); - } - }); - }; - const removePattern = () => { async function doRemove() { if (indexPattern.id === defaultIndex) { @@ -190,7 +167,6 @@ export const EditIndexPattern = withRouter( diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx index 8ca8c6453c7e96..aeeea6dec9a58d 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -26,7 +26,6 @@ interface IndexHeaderProps { indexPattern: IIndexPattern; defaultIndex?: string; setDefault?: () => void; - refreshFields?: () => void; deleteIndexPatternClick?: () => void; } @@ -44,14 +43,6 @@ const setDefaultTooltip = i18n.translate( } ); -const refreshAriaLabel = i18n.translate('indexPatternManagement.editIndexPattern.refreshAria', { - defaultMessage: 'Reload field list.', -}); - -const refreshTooltip = i18n.translate('indexPatternManagement.editIndexPattern.refreshTooltip', { - defaultMessage: 'Refresh field list.', -}); - const removeAriaLabel = i18n.translate('indexPatternManagement.editIndexPattern.removeAria', { defaultMessage: 'Remove index pattern.', }); @@ -64,7 +55,6 @@ export function IndexHeader({ defaultIndex, indexPattern, setDefault, - refreshFields, deleteIndexPatternClick, }: IndexHeaderProps) { return ( @@ -90,20 +80,6 @@ export function IndexHeader({ )} - {refreshFields && ( - - - - - - )} - {deleteIndexPatternClick && ( diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 5c29dfafd3c07b..f7f6af6e3cc1b4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -74,9 +74,11 @@ const filterPlaceholder = i18n.translate( ); export function Tabs({ indexPattern, saveIndexPattern, fields, history, location }: TabsProps) { - const { uiSettings, indexPatternManagementStart, docLinks } = useKibana< - IndexPatternManagmentContext - >().services; + const { + uiSettings, + indexPatternManagementStart, + docLinks, + } = useKibana().services; const [fieldFilter, setFieldFilter] = useState(''); const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState(''); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx index 0f80bfa681bf3b..4b51ae478a1780 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx @@ -36,9 +36,7 @@ interface StaticLookupItem { index: number; } -export class StaticLookupFormatEditor extends DefaultFormatEditor< - StaticLookupFormatEditorFormatParams -> { +export class StaticLookupFormatEditor extends DefaultFormatEditor { static formatId = 'static_lookup'; onLookupChange = (newLookupParams: { value?: string; key?: string }, index: number) => { const lookupEntries = [...this.props.formatParams.lookupEntries]; diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 2876bd62273507..e184fcb338b0cc 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -47,9 +47,7 @@ export interface IndexPatternManagmentContext { getMlCardState: () => MlCardState; } -export type IndexPatternManagmentContextValue = KibanaReactContextValue< - IndexPatternManagmentContext ->; +export type IndexPatternManagmentContextValue = KibanaReactContextValue; export enum MlCardState { HIDDEN, diff --git a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts index 3d1dcdbef3f4b4..6ef666f5851205 100644 --- a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts +++ b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts @@ -19,12 +19,9 @@ import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; -export const createTopNavDirective: Injectable>; +export const createTopNavDirective: Injectable< + IDirectiveFactory +>; export const createTopNavHelper: ( options: unknown ) => Injectable>; diff --git a/src/plugins/kibana_legacy/public/paginate/paginate.js b/src/plugins/kibana_legacy/public/paginate/paginate.js index f424c33ba7b02f..6c181778721f3e 100644 --- a/src/plugins/kibana_legacy/public/paginate/paginate.js +++ b/src/plugins/kibana_legacy/public/paginate/paginate.js @@ -76,30 +76,30 @@ export function PaginateDirectiveProvider($parse, $compile) { self.init = function () { self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp]; - $scope.$watchMulti(['paginate.perPage', self.perPageProp, self.otherWidthGetter], function ( - vals, - oldVals - ) { - const intChanges = vals[0] !== oldVals[0]; - - if (intChanges) { - if (!setPerPage(self.perPage)) { - // if we are not able to set the external value, - // render now, otherwise wait for the external value - // to trigger the watcher again - self.renderList(); + $scope.$watchMulti( + ['paginate.perPage', self.perPageProp, self.otherWidthGetter], + function (vals, oldVals) { + const intChanges = vals[0] !== oldVals[0]; + + if (intChanges) { + if (!setPerPage(self.perPage)) { + // if we are not able to set the external value, + // render now, otherwise wait for the external value + // to trigger the watcher again + self.renderList(); + } + return; } - return; - } - self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp]; - if (self.perPage == null) { - self.perPage = ALL; - return; - } + self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp]; + if (self.perPage == null) { + self.perPage = ALL; + return; + } - self.renderList(); - }); + self.renderList(); + } + ); $scope.$watch('page', self.changePage); $scope.$watchCollection(self.getList, function (list) { diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts index e4ef43fe8d4432..8b4996109444ff 100644 --- a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts +++ b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts @@ -19,9 +19,6 @@ import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; -export const KbnAccessibleClickProvider: Injectable>; +export const KbnAccessibleClickProvider: Injectable< + IDirectiveFactory +>; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts index 3020147e95d983..54699a64aacdad 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts @@ -51,9 +51,9 @@ export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedO let toCreate: Map; do { toCreate = new Map(); - const { saved_objects: rawApplicationUsageTransactional } = await savedObjectsClient.find< - ApplicationUsageTransactional - >({ + const { + saved_objects: rawApplicationUsageTransactional, + } = await savedObjectsClient.find({ type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, perPage: 1000, // Process 1000 at a time as a compromise of speed and overload }); diff --git a/src/plugins/management/public/management_sections_service.ts b/src/plugins/management/public/management_sections_service.ts index b9dc2dd416d9a0..4d78ce32cf33f0 100644 --- a/src/plugins/management/public/management_sections_service.ts +++ b/src/plugins/management/public/management_sections_service.ts @@ -36,9 +36,10 @@ import { } from './types'; import { createGetterSetter } from '../../kibana_utils/public'; -const [getSectionsServiceStartPrivate, setSectionsServiceStartPrivate] = createGetterSetter< - ManagementSectionsStartPrivate ->('SectionsServiceStartPrivate'); +const [ + getSectionsServiceStartPrivate, + setSectionsServiceStartPrivate, +] = createGetterSetter('SectionsServiceStartPrivate'); export { getSectionsServiceStartPrivate }; diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index e4f784e121058a..84a35861b75960 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -216,9 +216,9 @@ describe('Saved Object', () => { return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then( (savedObject) => { - stubSavedObjectsClientCreate({ id: 'myId' } as SimpleSavedObject< - SavedObjectAttributes - >); + stubSavedObjectsClientCreate({ + id: 'myId', + } as SimpleSavedObject); return savedObject.save({ confirmOverwrite: false }).then(() => { expect(startMock.overlays.openModal).not.toHaveBeenCalled(); diff --git a/src/plugins/saved_objects_management/public/services/service_registry.ts b/src/plugins/saved_objects_management/public/services/service_registry.ts index d4dc6d6166e461..fc169c100c6b53 100644 --- a/src/plugins/saved_objects_management/public/services/service_registry.ts +++ b/src/plugins/saved_objects_management/public/services/service_registry.ts @@ -25,9 +25,7 @@ export interface SavedObjectsManagementServiceRegistryEntry { title: string; } -export type ISavedObjectsManagementServiceRegistry = PublicMethodsOf< - SavedObjectsManagementServiceRegistry ->; +export type ISavedObjectsManagementServiceRegistry = PublicMethodsOf; export class SavedObjectsManagementServiceRegistry { private readonly registry = new Map(); diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index ba8b5e1e46f58d..107216cfabed89 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -17,31 +17,20 @@ * under the License. */ +// ESLint disabled dot-notation we can access the private key telemetryService['http'] /* eslint-disable dot-notation */ -const mockMomentValueOf = jest.fn(); - -jest.mock('moment', () => { - return jest.fn().mockImplementation(() => { - return { - valueOf: mockMomentValueOf, - }; - }); -}); import { mockTelemetryService } from '../mocks'; describe('TelemetryService', () => { describe('fetchTelemetry', () => { it('calls expected URL with 20 minutes - now', async () => { - const timestamp = Date.now(); - mockMomentValueOf.mockReturnValueOnce(timestamp); const telemetryService = mockTelemetryService(); await telemetryService.fetchTelemetry(); expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { - body: JSON.stringify({ unencrypted: false, timestamp }), + body: JSON.stringify({ unencrypted: false }), }); - expect(mockMomentValueOf).toBeCalled(); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index fade614be6c65e..3781670d5bb3c2 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -17,7 +17,6 @@ * under the License. */ -import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; import { TelemetryPluginConfig } from '../plugin'; @@ -124,7 +123,6 @@ export class TelemetryService { return this.http.post('/api/telemetry/v2/clusters/_stats', { body: JSON.stringify({ unencrypted, - timestamp: moment().valueOf(), }), }); }; diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index 83a35b22d2f307..f1f0b09ad708db 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -17,7 +17,6 @@ * under the License. */ -import moment from 'moment'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; @@ -86,7 +85,6 @@ export function registerTelemetryOptInRoutes({ } const statsGetterConfig: StatsGetterConfig = { - timestamp: moment().valueOf(), unencrypted: false, }; diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts index ff2e0be1aa1aeb..2f7ce23c7ad9b9 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts @@ -19,7 +19,6 @@ // @ts-ignore import fetch from 'node-fetch'; -import moment from 'moment'; import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; @@ -72,7 +71,6 @@ export function registerTelemetryOptInStatsRoutes( const unencrypted = req.body.unencrypted; const statsGetterConfig: StatsGetterConfig = { - timestamp: moment().valueOf(), unencrypted, request: req, }; diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index fd888cbe087f75..0a92fe4c623eb9 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -17,21 +17,13 @@ * under the License. */ -import moment from 'moment'; import { schema } from '@kbn/config-schema'; -import { TypeOptions } from '@kbn/config-schema/target/types/types'; import { IRouter } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup, StatsGetterConfig, } from 'src/plugins/telemetry_collection_manager/server'; -const validate: TypeOptions['validate'] = (value) => { - if (!moment(value).isValid()) { - return `${value} is not a valid date`; - } -}; - export function registerTelemetryUsageStatsRoutes( router: IRouter, telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, @@ -43,16 +35,14 @@ export function registerTelemetryUsageStatsRoutes( validate: { body: schema.object({ unencrypted: schema.boolean({ defaultValue: false }), - timestamp: schema.oneOf([schema.string({ validate }), schema.number({ validate })]), }), }, }, async (context, req, res) => { - const { unencrypted, timestamp } = req.body; + const { unencrypted } = req.body; try { const statsConfig: StatsGetterConfig = { - timestamp: moment(timestamp).valueOf(), request: req, unencrypted, }; diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index c9e2f22fa19aa9..bc33e9fbc82c5b 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -144,7 +144,7 @@ export class TelemetryCollectionManagerPlugin collectionSoService: SavedObjectsServiceStart, usageCollection: UsageCollectionSetup ): StatsCollectionConfig { - const { timestamp, request } = config; + const { request } = config; const callCluster = config.unencrypted ? collection.esCluster.asScoped(request).callAsCurrentUser @@ -160,7 +160,7 @@ export class TelemetryCollectionManagerPlugin // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted const kibanaRequest = config.unencrypted ? request : void 0; - return { callCluster, timestamp, usageCollection, esClient, soClient, kibanaRequest }; + return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 7d25b8c8261c4d..a6cf1a9e5aaf95 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -56,7 +56,6 @@ export interface TelemetryOptInStats { export interface BaseStatsGetterConfig { unencrypted: boolean; - timestamp: number; request?: KibanaRequest; } @@ -76,7 +75,6 @@ export interface ClusterDetails { export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; callCluster: LegacyAPICaller; - timestamp: number; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract | ISavedObjectsRepository; kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter diff --git a/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx index 2173d92819e3d3..cd8078a739b3ec 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx @@ -60,9 +60,9 @@ describe('NumberRow', () => { test('should call onChange', () => { const comp = shallow(); - comp.find('EuiFieldNumber').prop('onChange')!({ target: { value: '5' } } as React.ChangeEvent< - HTMLInputElement - >); + comp.find('EuiFieldNumber').prop('onChange')!({ + target: { value: '5' }, + } as React.ChangeEvent); expect(defaultProps.onChange).lastCalledWith({ id: defaultProps.model.id, value: '5' }); }); diff --git a/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts index 6a549ac6f8e0a1..47c8cfd8cf1342 100644 --- a/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts +++ b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts @@ -27,6 +27,7 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Search'); -export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter< - SavedObjectsClientContract ->('SavedObjectsClient'); +export const [ + getSavedObjectsClient, + setSavedObjectsClient, +] = createGetterSetter('SavedObjectsClient'); diff --git a/src/plugins/vis_type_timelion/server/series_functions/legend.js b/src/plugins/vis_type_timelion/server/series_functions/legend.js index aae5b723e30bf4..2f7d526d6e461b 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/legend.js +++ b/src/plugins/vis_type_timelion/server/series_functions/legend.js @@ -113,27 +113,24 @@ export default new Chainable('legend', { defaultMessage: 'Set the position and style of the legend on the plot', }), fn: function legendFn(args) { - return alter(args, function ( - eachSeries, - position, - columns, - showTime = true, - timeFormat = DEFAULT_TIME_FORMAT - ) { - eachSeries._global = eachSeries._global || {}; - eachSeries._global.legend = eachSeries._global.legend || {}; - eachSeries._global.legend.noColumns = columns; - eachSeries._global.legend.showTime = showTime; - eachSeries._global.legend.timeFormat = timeFormat; + return alter( + args, + function (eachSeries, position, columns, showTime = true, timeFormat = DEFAULT_TIME_FORMAT) { + eachSeries._global = eachSeries._global || {}; + eachSeries._global.legend = eachSeries._global.legend || {}; + eachSeries._global.legend.noColumns = columns; + eachSeries._global.legend.showTime = showTime; + eachSeries._global.legend.timeFormat = timeFormat; - if (position === false) { - eachSeries._global.legend.show = false; - eachSeries._global.legend.showTime = false; - } else { - eachSeries._global.legend.position = position; - } + if (position === false) { + eachSeries._global.legend.show = false; + eachSeries._global.legend.showTime = false; + } else { + eachSeries._global.legend.position = position; + } - return eachSeries; - }); + return eachSeries; + } + ); }, }); diff --git a/src/plugins/vis_type_timelion/server/series_functions/yaxis.js b/src/plugins/vis_type_timelion/server/series_functions/yaxis.js index 7a249305ee76ea..2c5bcccde5484c 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/yaxis.js +++ b/src/plugins/vis_type_timelion/server/series_functions/yaxis.js @@ -106,86 +106,79 @@ export default new Chainable('yaxis', { 'Configures a variety of y-axis options, the most important likely being the ability to add an Nth (eg 2nd) y-axis', }), fn: function yaxisFn(args) { - return alter(args, function ( - eachSeries, - yaxis, - min, - max, - position, - label, - color, - units, - tickDecimals - ) { - yaxis = yaxis || 1; + return alter( + args, + function (eachSeries, yaxis, min, max, position, label, color, units, tickDecimals) { + yaxis = yaxis || 1; - eachSeries.yaxis = yaxis; - eachSeries._global = eachSeries._global || {}; + eachSeries.yaxis = yaxis; + eachSeries._global = eachSeries._global || {}; - eachSeries._global.yaxes = eachSeries._global.yaxes || []; - eachSeries._global.yaxes[yaxis - 1] = eachSeries._global.yaxes[yaxis - 1] || {}; + eachSeries._global.yaxes = eachSeries._global.yaxes || []; + eachSeries._global.yaxes[yaxis - 1] = eachSeries._global.yaxes[yaxis - 1] || {}; - const myAxis = eachSeries._global.yaxes[yaxis - 1]; - myAxis.position = position || (yaxis % 2 ? 'left' : 'right'); - myAxis.min = min; - myAxis.max = max; - myAxis.axisLabelFontSizePixels = 11; - myAxis.axisLabel = label; - myAxis.axisLabelColour = color; - myAxis.axisLabelUseCanvas = true; + const myAxis = eachSeries._global.yaxes[yaxis - 1]; + myAxis.position = position || (yaxis % 2 ? 'left' : 'right'); + myAxis.min = min; + myAxis.max = max; + myAxis.axisLabelFontSizePixels = 11; + myAxis.axisLabel = label; + myAxis.axisLabelColour = color; + myAxis.axisLabelUseCanvas = true; - if (tickDecimals) { - myAxis.tickDecimals = tickDecimals < 0 ? 0 : tickDecimals; - } - - if (units) { - const unitTokens = units.split(':'); - const unitType = unitTokens[0]; - if (!tickFormatters[unitType]) { - throw new Error( - i18n.translate( - 'timelion.serverSideErrors.yaxisFunction.notSupportedUnitTypeErrorMessage', - { - defaultMessage: '{units} is not a supported unit type.', - values: { units }, - } - ) - ); + if (tickDecimals) { + myAxis.tickDecimals = tickDecimals < 0 ? 0 : tickDecimals; } - if (unitType === 'currency') { - const threeLetterCode = /^[A-Za-z]{3}$/; - const currency = unitTokens[1]; - if (currency && !threeLetterCode.test(currency)) { + + if (units) { + const unitTokens = units.split(':'); + const unitType = unitTokens[0]; + if (!tickFormatters[unitType]) { throw new Error( i18n.translate( - 'timelion.serverSideErrors.yaxisFunction.notValidCurrencyFormatErrorMessage', + 'timelion.serverSideErrors.yaxisFunction.notSupportedUnitTypeErrorMessage', { - defaultMessage: 'Currency must be a three letter code', + defaultMessage: '{units} is not a supported unit type.', + values: { units }, } ) ); } - } + if (unitType === 'currency') { + const threeLetterCode = /^[A-Za-z]{3}$/; + const currency = unitTokens[1]; + if (currency && !threeLetterCode.test(currency)) { + throw new Error( + i18n.translate( + 'timelion.serverSideErrors.yaxisFunction.notValidCurrencyFormatErrorMessage', + { + defaultMessage: 'Currency must be a three letter code', + } + ) + ); + } + } - myAxis.units = { - type: unitType, - prefix: unitTokens[1] || '', - suffix: unitTokens[2] || '', - }; + myAxis.units = { + type: unitType, + prefix: unitTokens[1] || '', + suffix: unitTokens[2] || '', + }; - if (unitType === 'percent') { - // jquery.flot uses axis.tickDecimals to generate tick values - // need 2 extra decimal places to preserve precision when percent shifts value to left - myAxis.units.tickDecimalsShift = 2; - if (tickDecimals) { - myAxis.tickDecimals += myAxis.units.tickDecimalsShift; - } else { - myAxis.tickDecimals = myAxis.units.tickDecimalsShift; + if (unitType === 'percent') { + // jquery.flot uses axis.tickDecimals to generate tick values + // need 2 extra decimal places to preserve precision when percent shifts value to left + myAxis.units.tickDecimalsShift = 2; + if (tickDecimals) { + myAxis.tickDecimals += myAxis.units.tickDecimalsShift; + } else { + myAxis.tickDecimals = myAxis.units.tickDecimalsShift; + } } } - } - return eachSeries; - }); + return eachSeries; + } + ); }, }); 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 05836a6df410a5..a045dbf38c1f9f 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/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js index bb3f0041abca7f..b2ea90d6a87fe1 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/visualizations/public/saved_visualizations/find_list_items.test.ts b/src/plugins/visualizations/public/saved_visualizations/find_list_items.test.ts index 3acfcb5230cb1d..a7c15dbfed8d8e 100644 --- a/src/plugins/visualizations/public/saved_visualizations/find_list_items.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/find_list_items.test.ts @@ -24,9 +24,8 @@ import { VisTypeAlias } from '../vis_types'; describe('saved_visualizations', () => { function testProps() { - const savedObjects = coreMock.createStart().savedObjects.client as jest.Mocked< - SavedObjectsClientContract - >; + const savedObjects = coreMock.createStart().savedObjects + .client as jest.Mocked; (savedObjects.find as jest.Mock).mockImplementation(() => ({ total: 0, savedObjects: [], diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 08537b4ac30810..1c7dd7a72e3bcf 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -83,9 +83,10 @@ export const [getExpressions, setExpressions] = createGetterSetter('UiActions'); -export const [getSavedVisualizationsLoader, setSavedVisualizationsLoader] = createGetterSetter< - SavedVisualizationsLoader ->('SavedVisualisationsLoader'); +export const [ + getSavedVisualizationsLoader, + setSavedVisualizationsLoader, +] = createGetterSetter('SavedVisualisationsLoader'); export const [getAggs, setAggs] = createGetterSetter( 'AggConfigs' diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index 854b462be7704e..b3d34d5910fc3b 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -53,12 +53,10 @@ export default function ({ getService }) { }); it('should pull local stats and validate data types', async () => { - const timestamp = '2018-07-23T22:13:00Z'; - const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); expect(body.length).to.be(1); @@ -95,12 +93,10 @@ export default function ({ getService }) { }); it('should pull local stats and validate fields', async () => { - const timestamp = '2018-07-23T22:13:00Z'; - const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); const stats = body[0]; @@ -150,8 +146,6 @@ export default function ({ getService }) { }); describe('application usage limits', () => { - const timestamp = '2018-07-23T22:13:00Z'; - function createSavedObject() { return supertest .post('/api/saved_objects/application_usage_transactional') @@ -182,7 +176,7 @@ export default function ({ getService }) { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); expect(body.length).to.be(1); @@ -233,7 +227,7 @@ export default function ({ getService }) { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); expect(body.length).to.be(1); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 530b8e1111a0c6..e2fcf50ef2c12c 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }) { // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); log.debug('popularity = ' + popularity); - expect(popularity).to.be('0'); + expect(popularity).to.be(''); }); it('can be saved', async function () { diff --git a/test/functional/apps/timelion/_expression_typeahead.js b/test/functional/apps/timelion/_expression_typeahead.js index d1e974942a362e..5d834f1a055ded 100644 --- a/test/functional/apps/timelion/_expression_typeahead.js +++ b/test/functional/apps/timelion/_expression_typeahead.js @@ -86,7 +86,7 @@ export default function ({ getPageObjects }) { await PageObjects.timelion.updateExpression(',split'); await PageObjects.timelion.clickSuggestion(); const suggestions = await PageObjects.timelion.getSuggestionItemsText(); - expect(suggestions.length).to.eql(52); + expect(suggestions.length).to.eql(51); expect(suggestions[0].includes('@message.raw')).to.eql(true); await PageObjects.timelion.clickSuggestion(10); }); diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index b3b7fd32eae19f..8f03b1d7602582 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/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json b/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json new file mode 100644 index 00000000000000..14d67f9bfbc34d --- /dev/null +++ b/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json @@ -0,0 +1,4229 @@ +{ + "type": "doc", + "value": { + "id": "search:a16d1990-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "animal", + "isDog", + "name", + "sound", + "weightLbs" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:>40\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "weightLbs", + "desc" + ] + ], + "title": "animal weights", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-11T20:55:26.317Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.3.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-11T20:43:55.434Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:61c58ad0-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "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": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.3.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.3.0\",\"panelIndex\":\"2\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "dashboard with filter", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "panel_1", + "type": "search" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:2ae34a60-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.3.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.3.0\",\"panelIndex\":\"2\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "couple panels", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "145ced90-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "e2023110-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:03:29.670Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:76d03330-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "and_descriptions_has_underscores", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "dashboard_with_underscores", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:58:27.555Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:9b780cd0-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "* hi & $%!!@# 漢字 ^--=++[]{};'~`~<>?,./:\";'\\|\\\\ special chars", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:00:07.322Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6c0b16e0-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "dashboard-name-has-dashes", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:58:09.486Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:19523860-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "im empty too", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:03:00.198Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:14616b50-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "im empty", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:02:51.909Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:33bb8ad0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"2\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"}]", + "timeRestore": false, + "title": "few panels", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "145ced90-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "e2023110-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_2", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:03:44.509Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:60659030-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 2", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:59.443Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:65227c00-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 3", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:07.392Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6803a2f0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 4", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:12.223Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6b18f940-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 5", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:17.396Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6e12ff60-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 6", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:22.390Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:4f0fd980-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:30.360Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:3de0bda0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "1", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:01.530Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:46c8b580-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "2", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:16.472Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:708fe640-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 7", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:26.564Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:7b8d50a0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "Hi i have a lot of words in my dashboard name! It's pretty long i wonder what it'll look like", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:45.002Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:7e42d3b0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "bye", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:49.547Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:846988b0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "last", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:59.867Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:cbd3bc30-3e5a-11e8-9fc3-39e49624228e", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"weightLbs:<50\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"name.keyword\",\"value\":\"Fee Fee\",\"params\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"name.keyword\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":true,\"useMargins\":true,\"hidePanelTitles\":true}", + "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"2\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"}]", + "timeRestore": false, + "title": "bug", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "771b4f10-3e59-11e8-9fc3-39e49624228e", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "befdb6b0-3e59-11e8-9fc3-39e49624228e", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "4c0c3f90-3e5a-11e8-9fc3-39e49624228e", + "name": "panel_2", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-12T14:07:12.243Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:5bac3a80-3e5b-11e8-9fc3-39e49624228e", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "dashboard with scripted filter, negated filter and query", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:<50\"},\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"key\":\"name.keyword\",\"negate\":true,\"params\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"},\"type\":\"phrase\",\"value\":\"Fee Fee\",\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"name.keyword\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"}}}},{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":\"is dog\",\"disabled\":false,\"field\":\"isDog\",\"key\":\"isDog\",\"negate\":false,\"params\":{\"value\":true},\"type\":\"phrase\",\"value\":\"true\",\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index\"},\"script\":{\"script\":{\"inline\":\"boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { return doc['animal.keyword'].value == 'dog' }, params.value);\",\"lang\":\"painless\",\"params\":{\"value\":true}}}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":true,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"3\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"4\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"4\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "section": 0, + "value": 0 + }, + "timeFrom": "Wed Apr 12 2017 10:06:21 GMT-0400", + "timeRestore": true, + "timeTo": "Thu Apr 12 2018 10:06:21 GMT-0400", + "title": "filters", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index", + "type": "index-pattern" + }, + { + "id": "771b4f10-3e59-11e8-9fc3-39e49624228e", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "4c0c3f90-3e5a-11e8-9fc3-39e49624228e", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "name": "panel_2", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-12T14:11:13.576Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"activity level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"barking level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"breed\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"breed.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"size.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"trainability\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "dogbreeds" + }, + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-04-12T16:24:29.357Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:84908bb0-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.10425, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:04:37.610Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:04:37.611Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:84908bb1-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "home", + "minutesOnScreen": 0.5708666666666666, + "numberOfClicks": 1, + "timestamp": "2020-05-31T11:04:37.610Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:04:37.611Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:951d7420-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 1.3920166666666667, + "numberOfClicks": 39, + "timestamp": "2020-05-31T11:05:05.378Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:05:05.378Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:a79fc3f0-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.45816666666666667, + "numberOfClicks": 8, + "timestamp": "2020-05-31T11:05:36.431Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:05:36.431Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:bc30cf80-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.5057333333333333, + "numberOfClicks": 14, + "timestamp": "2020-05-31T11:06:10.935Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:06:10.936Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:df4781d0-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.3740833333333333, + "numberOfClicks": 1, + "timestamp": "2020-05-31T11:07:09.804Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:07:09.805Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:dffd3d40-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.02105, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:07:10.996Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:07:10.996Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:e05dd3d0-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.010416666666666666, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:07:11.629Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:07:11.629Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:cfc85fe0-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.5466333333333333, + "numberOfClicks": 16, + "timestamp": "2020-05-31T11:06:43.806Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:06:43.806Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:00710a20-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.8720333333333333, + "numberOfClicks": 14, + "timestamp": "2020-05-31T11:08:05.442Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:08:05.442Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:e1454da0-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.01815, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:07:13.146Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:07:13.146Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:0eb5d750-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.33895, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:08:29.380Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:08:29.381Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:2e09dc50-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.08756666666666667, + "numberOfClicks": 3, + "timestamp": "2020-05-31T11:09:21.941Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:21.941Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:25916f20-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.5207166666666667, + "numberOfClicks": 5, + "timestamp": "2020-05-31T11:09:07.730Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:07.730Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:1e9c4690-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.38158333333333333, + "numberOfClicks": 17, + "timestamp": "2020-05-31T11:08:56.057Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:08:56.057Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:1f41ae50-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.0183, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:08:57.141Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:08:57.141Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:26923d50-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.0285, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:09.413Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:09.413Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:1024f7b0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.0401, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:08:31.787Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:08:31.787Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:10f7d810-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.012, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:08:33.169Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:08:33.169Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:34f010c0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.28990000000000005, + "numberOfClicks": 4, + "timestamp": "2020-05-31T11:09:33.515Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:33.516Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:35665230-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.012216666666666667, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:34.291Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:34.291Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:404deaa0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.24620000000000003, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:52.586Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:52.586Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:415ac6c0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.015816666666666666, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:54.348Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:54.348Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:40c898e0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.013533333333333333, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:53.390Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:53.390Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:434a13a0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.011566666666666666, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:57.594Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:57.594Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:42de5980-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.042, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:09:56.888Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:09:56.888Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:ffde98b0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 2.983433333333333, + "numberOfClicks": 31, + "timestamp": "2020-05-31T11:15:13.979Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:15:13.979Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:46ca9260-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.3946, + "numberOfClicks": 4, + "timestamp": "2020-05-31T11:17:12.966Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:17:12.966Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:47072630-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.006116666666666667, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:17:13.363Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:17:13.363Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:3805bfc0-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.19001666666666667, + "numberOfClicks": 2, + "timestamp": "2020-05-31T11:16:48.188Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:16:48.188Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:38af4630-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.018766666666666668, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:16:49.299Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:16:49.299Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:3136e3e0-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.012466666666666666, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:16:36.766Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:16:36.766Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:fd3cebc0-a32f-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.5283, + "numberOfClicks": 17, + "timestamp": "2020-05-31T11:15:09.564Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:15:09.564Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6463c800-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.28708333333333336, + "numberOfClicks": 10, + "timestamp": "2020-05-31T11:18:02.624Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:02.624Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:57b7c340-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.4411833333333333, + "numberOfClicks": 11, + "timestamp": "2020-05-31T11:17:41.364Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:17:41.364Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:67dbd7c0-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 2.4508833333333335, + "numberOfClicks": 5, + "timestamp": "2020-05-31T11:18:08.444Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:08.444Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6bf41f20-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 2.5884833333333335, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:18:15.314Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:15.314Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6c5ccc00-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.011383333333333334, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:18:16.000Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:16.000Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6ce7f500-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.015066666666666667, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:18:16.912Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:16.912Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:5a178530-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.067, + "numberOfClicks": 3, + "timestamp": "2020-05-31T11:17:45.347Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:17:45.347Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:d10c66b0-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.030199999999999998, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:21:04.923Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:21:04.923Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:cd5aa950-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 2.2735666666666665, + "numberOfClicks": 21, + "timestamp": "2020-05-31T11:20:58.724Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:20:58.725Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:b2dee050-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.08906666666666667, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:20:14.293Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:20:14.293Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:d194a980-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.018183333333333333, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:21:05.816Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:21:05.816Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:72b406e0-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.14396666666666666, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:18:26.638Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:26.638Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6d8e6e30-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.0179, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:18:18.003Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:18:18.003Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:5e085850-3e6e-11e8-bbb9-e15942d5d48c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-12T16:27:17.973Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "non timebased line chart - dog data", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"non timebased line chart - dog data\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Max trainability\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Max trainability\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true},{\"show\":true,\"mode\":\"normal\",\"type\":\"line\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"3\",\"label\":\"Max barking level\"},\"valueAxis\":\"ValueAxis-1\"},{\"show\":true,\"mode\":\"normal\",\"type\":\"line\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"4\",\"label\":\"Max activity level\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"trainability\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"breed.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"barking level\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"activity level\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:a5d56330-3e6e-11e8-bbb9-e15942d5d48c", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "I have two visualizations that are created off a non time based index", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.3.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.3.0\",\"panelIndex\":\"2\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Non time based", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "5e085850-3e6e-11e8-bbb9-e15942d5d48c", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-12T16:29:18.435Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:d2525040-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "I have one of every visualization type since the last time I was created!", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"2\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_2\"},{\"panelIndex\":\"4\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"4\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_3\"},{\"panelIndex\":\"5\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"5\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_4\"},{\"panelIndex\":\"6\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"6\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_5\"},{\"panelIndex\":\"7\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"7\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_6\"},{\"panelIndex\":\"8\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"8\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_7\"},{\"panelIndex\":\"9\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_8\"},{\"panelIndex\":\"10\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"10\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_9\"},{\"panelIndex\":\"11\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"11\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_10\"},{\"panelIndex\":\"12\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"12\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_11\"},{\"panelIndex\":\"13\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"13\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_12\"},{\"panelIndex\":\"14\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"14\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_13\"},{\"panelIndex\":\"15\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"15\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_14\"},{\"panelIndex\":\"16\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"16\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_15\"},{\"panelIndex\":\"17\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"17\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_16\"},{\"panelIndex\":\"18\",\"gridData\":{\"x\":24,\"y\":120,\"w\":24,\"h\":15,\"i\":\"18\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_17\"},{\"panelIndex\":\"19\",\"gridData\":{\"x\":0,\"y\":135,\"w\":24,\"h\":15,\"i\":\"19\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_18\"},{\"panelIndex\":\"20\",\"gridData\":{\"x\":24,\"y\":135,\"w\":24,\"h\":15,\"i\":\"20\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_19\"},{\"panelIndex\":\"21\",\"gridData\":{\"x\":0,\"y\":150,\"w\":24,\"h\":15,\"i\":\"21\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_20\"},{\"panelIndex\":\"22\",\"gridData\":{\"x\":24,\"y\":150,\"w\":24,\"h\":15,\"i\":\"22\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_21\"},{\"panelIndex\":\"23\",\"gridData\":{\"x\":0,\"y\":165,\"w\":24,\"h\":15,\"i\":\"23\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_22\"},{\"panelIndex\":\"24\",\"gridData\":{\"x\":24,\"y\":165,\"w\":24,\"h\":15,\"i\":\"24\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_23\"},{\"panelIndex\":\"25\",\"gridData\":{\"x\":0,\"y\":180,\"w\":24,\"h\":15,\"i\":\"25\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_24\"},{\"panelIndex\":\"26\",\"gridData\":{\"x\":24,\"y\":180,\"w\":24,\"h\":15,\"i\":\"26\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_25\"},{\"panelIndex\":\"27\",\"gridData\":{\"x\":0,\"y\":195,\"w\":24,\"h\":15,\"i\":\"27\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_26\"},{\"panelIndex\":\"28\",\"gridData\":{\"x\":24,\"y\":195,\"w\":24,\"h\":15,\"i\":\"28\"},\"embeddableConfig\":{},\"version\":\"7.3.0\",\"panelRefName\":\"panel_27\"},{\"panelIndex\":\"29\",\"gridData\":{\"x\":0,\"y\":210,\"w\":24,\"h\":15,\"i\":\"29\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_28\"},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":210,\"i\":\"30\"},\"version\":\"7.3.0\",\"panelIndex\":\"30\",\"embeddableConfig\":{},\"panelRefName\":\"panel_29\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "dashboard with everything", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "e6140540-3dca-11e8-8660-4d65aa086b3c", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "3525b840-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_2", + "type": "visualization" + }, + { + "id": "37a541c0-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ffa2e0c0-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "e2023110-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "145ced90-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_6", + "type": "visualization" + }, + { + "id": "2d1b1620-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_7", + "type": "visualization" + }, + { + "id": "42535e30-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_8", + "type": "visualization" + }, + { + "id": "42535e30-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_9", + "type": "visualization" + }, + { + "id": "4c0f47e0-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_10", + "type": "visualization" + }, + { + "id": "11ae2bd0-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_11", + "type": "visualization" + }, + { + "id": "3fe22200-3dcb-11e8-8660-4d65aa086b3c", + "name": "panel_12", + "type": "visualization" + }, + { + "id": "4ca00ba0-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_13", + "type": "visualization" + }, + { + "id": "78803be0-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_14", + "type": "visualization" + }, + { + "id": "b92ae920-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_15", + "type": "visualization" + }, + { + "id": "e4d8b430-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_16", + "type": "visualization" + }, + { + "id": "f81134a0-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_17", + "type": "visualization" + }, + { + "id": "cc43fab0-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_18", + "type": "visualization" + }, + { + "id": "02a2e4e0-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_19", + "type": "visualization" + }, + { + "id": "df815d20-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_20", + "type": "visualization" + }, + { + "id": "c40f4d40-3dcc-11e8-8660-4d65aa086b3c", + "name": "panel_21", + "type": "visualization" + }, + { + "id": "7fda8ee0-3dcd-11e8-8660-4d65aa086b3c", + "name": "panel_22", + "type": "visualization" + }, + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "panel_23", + "type": "search" + }, + { + "id": "be5accf0-3dca-11e8-8660-4d65aa086b3c", + "name": "panel_24", + "type": "search" + }, + { + "id": "ca5ada40-3dca-11e8-8660-4d65aa086b3c", + "name": "panel_25", + "type": "search" + }, + { + "id": "771b4f10-3e59-11e8-9fc3-39e49624228e", + "name": "panel_26", + "type": "visualization" + }, + { + "id": "5e085850-3e6e-11e8-bbb9-e15942d5d48c", + "name": "panel_27", + "type": "visualization" + }, + { + "id": "8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c", + "name": "panel_28", + "type": "visualization" + }, + { + "id": "befdb6b0-3e59-11e8-9fc3-39e49624228e", + "name": "panel_29", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-16T16:05:02.915Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:29bd0240-4197-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-16T16:56:53.092Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"CN\",\"params\":{\"query\":\"CN\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"CN\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"bytes >= 10000\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Kuery: pie bytes with kuery and filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Kuery: pie bytes with kuery and filter\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fieldFormatMap": "{\"machine.ram\":{\"id\":\"number\",\"params\":{\"pattern\":\"0,0.[000] b\"}}}", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-04-16T16:57:12.263Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "search:55d37a30-4197-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "agent", + "bytes", + "clientip" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"clientip : 73.14.212.83\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"range\",\"key\":\"bytes\",\"value\":\"100 to 1,000\",\"params\":{\"gte\":100,\"lt\":1000},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"range\":{\"bytes\":{\"gte\":100,\"lt\":1000}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Bytes and kuery in saved search with filter", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-16T16:58:07.059Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:b60de070-4197-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "Bytes bytes and more bytes", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"2\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"},{\"panelIndex\":\"4\",\"gridData\":{\"x\":24,\"y\":15,\"w\":17,\"h\":8,\"i\":\"4\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_3\"},{\"panelIndex\":\"5\",\"gridData\":{\"x\":0,\"y\":30,\"w\":18,\"h\":13,\"i\":\"5\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_4\"},{\"panelIndex\":\"6\",\"gridData\":{\"x\":24,\"y\":37,\"w\":24,\"h\":12,\"i\":\"6\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_5\"},{\"panelIndex\":\"7\",\"gridData\":{\"x\":18,\"y\":30,\"w\":9,\"h\":7,\"i\":\"7\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_6\"},{\"panelIndex\":\"8\",\"gridData\":{\"x\":28,\"y\":23,\"w\":15,\"h\":13,\"i\":\"8\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_7\"},{\"panelIndex\":\"9\",\"gridData\":{\"x\":0,\"y\":43,\"w\":24,\"h\":15,\"i\":\"9\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_8\"},{\"panelIndex\":\"10\",\"gridData\":{\"x\":24,\"y\":49,\"w\":18,\"h\":12,\"i\":\"10\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_9\"},{\"panelIndex\":\"11\",\"gridData\":{\"x\":0,\"y\":58,\"w\":24,\"h\":15,\"i\":\"11\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_10\"},{\"panelIndex\":\"12\",\"gridData\":{\"x\":24,\"y\":61,\"w\":5,\"h\":4,\"i\":\"12\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_11\"},{\"panelIndex\":\"13\",\"gridData\":{\"x\":0,\"y\":73,\"w\":17,\"h\":6,\"i\":\"13\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_12\"},{\"panelIndex\":\"14\",\"gridData\":{\"x\":24,\"y\":65,\"w\":24,\"h\":15,\"i\":\"14\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_13\"},{\"panelIndex\":\"15\",\"gridData\":{\"x\":0,\"y\":79,\"w\":24,\"h\":6,\"i\":\"15\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_14\"},{\"panelIndex\":\"16\",\"gridData\":{\"x\":24,\"y\":80,\"w\":24,\"h\":15,\"i\":\"16\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_15\"},{\"panelIndex\":\"17\",\"gridData\":{\"x\":0,\"y\":85,\"w\":13,\"h\":11,\"i\":\"17\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_16\"},{\"panelIndex\":\"18\",\"gridData\":{\"x\":24,\"y\":95,\"w\":23,\"h\":11,\"i\":\"18\"},\"version\":\"7.3.0\",\"embeddableConfig\":{},\"panelRefName\":\"panel_17\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "All about those bytes", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "7ff2c4c0-4191-11e8-bb13-d53698fb349a", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "03d2afd0-4192-11e8-bb13-d53698fb349a", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "63983430-4192-11e8-bb13-d53698fb349a", + "name": "panel_2", + "type": "visualization" + }, + { + "id": "0ca8c600-4195-11e8-bb13-d53698fb349a", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "c10c6b00-4191-11e8-bb13-d53698fb349a", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "760a9060-4190-11e8-bb13-d53698fb349a", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "1dcdfe30-4192-11e8-bb13-d53698fb349a", + "name": "panel_6", + "type": "visualization" + }, + { + "id": "584c0300-4191-11e8-bb13-d53698fb349a", + "name": "panel_7", + "type": "visualization" + }, + { + "id": "b3e70d00-4190-11e8-bb13-d53698fb349a", + "name": "panel_8", + "type": "visualization" + }, + { + "id": "df72ad40-4194-11e8-bb13-d53698fb349a", + "name": "panel_9", + "type": "visualization" + }, + { + "id": "9bebe980-4192-11e8-bb13-d53698fb349a", + "name": "panel_10", + "type": "visualization" + }, + { + "id": "9fb4c670-4194-11e8-bb13-d53698fb349a", + "name": "panel_11", + "type": "visualization" + }, + { + "id": "35417e50-4194-11e8-bb13-d53698fb349a", + "name": "panel_12", + "type": "visualization" + }, + { + "id": "039e4770-4194-11e8-bb13-d53698fb349a", + "name": "panel_13", + "type": "visualization" + }, + { + "id": "76c7f020-4194-11e8-bb13-d53698fb349a", + "name": "panel_14", + "type": "visualization" + }, + { + "id": "8090dcb0-4195-11e8-bb13-d53698fb349a", + "name": "panel_15", + "type": "visualization" + }, + { + "id": "29bd0240-4197-11e8-bb13-d53698fb349a", + "name": "panel_16", + "type": "visualization" + }, + { + "id": "55d37a30-4197-11e8-bb13-d53698fb349a", + "name": "panel_17", + "type": "search" + } + ], + "type": "dashboard", + "updated_at": "2018-04-16T17:00:48.503Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:78803be0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.127Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: tag cloud", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tag cloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:3fe22200-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.130Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4ca00ba0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.131Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: region map", + "uiStateJSON": "{\"mapZoom\":2,\"mapCenter\":[8.754794702435618,-9.140625000000002]}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: region map\",\"type\":\"region_map\",\"params\":{\"legendPosition\":\"bottomright\",\"addTooltip\":true,\"colorSchema\":\"Yellow to Red\",\"selectedLayer\":{\"attribution\":\"

Made with NaturalEarth | Elastic Maps Service

\",\"name\":\"World Countries\",\"weight\":1,\"format\":{\"type\":\"geojson\"},\"url\":\"https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=6.3.0\",\"fields\":[{\"name\":\"iso2\",\"description\":\"Two letter abbreviation\"},{\"name\":\"iso3\",\"description\":\"Three letter abbreviation\"},{\"name\":\"name\",\"description\":\"Country name\"}],\"created_at\":\"2017-07-31T16:00:19.996450\",\"tags\":[],\"id\":5715999101812736,\"layerId\":\"elastic_maps_service.World Countries\"},\"selectedJoinField\":{\"name\":\"iso2\",\"description\":\"Two letter abbreviation\"},\"isDisplayWarning\":true,\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

© OpenStreetMap contributors | Elastic Maps Service

\",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

© OpenStreetMap contributors | Elastic Maps Service

\",\"subdomains\":[]}},\"mapZoom\":2,\"mapCenter\":[0,0],\"outlineWeight\":1,\"showAllShapes\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:11ae2bd0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.133Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: metric", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: metric\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:145ced90-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.134Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: heatmap", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 15\":\"rgb(247,252,245)\",\"15 - 30\":\"rgb(199,233,192)\",\"30 - 45\":\"rgb(116,196,118)\",\"45 - 60\":\"rgb(35,139,69)\"}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: heatmap\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Greens\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":false,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:e2023110-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.135Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: guage", + "uiStateJSON": "{\"vis\":{\"colors\":{\"0 - 50000\":\"#EF843C\",\"75000 - 10000000\":\"#3F6833\"},\"defaultColors\":{\"0 - 5000000\":\"rgb(0,104,55)\",\"50000000 - 74998990099\":\"rgb(165,0,38)\"}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: guage\",\"type\":\"gauge\",\"params\":{\"addLegend\":true,\"addTooltip\":true,\"gauge\":{\"backStyle\":\"Full\",\"colorSchema\":\"Green to Red\",\"colorsRange\":[{\"from\":0,\"to\":5000000},{\"from\":50000000,\"to\":74998990099}],\"extendRange\":true,\"gaugeColorMode\":\"Labels\",\"gaugeStyle\":\"Full\",\"gaugeType\":\"Arc\",\"invertColors\":false,\"labels\":{\"color\":\"black\",\"show\":true},\"orientation\":\"vertical\",\"percentageMode\":false,\"scale\":{\"color\":\"#333\",\"labels\":false,\"show\":true},\"style\":{\"bgColor\":false,\"bgFill\":\"#eee\",\"bgMask\":false,\"bgWidth\":0.9,\"fontSize\":60,\"labelColor\":true,\"mask\":false,\"maskBars\":50,\"subText\":\"\",\"width\":0.9},\"type\":\"meter\",\"alignment\":\"horizontal\"},\"isDisplayWarning\":false,\"type\":\"gauge\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"machine.ram\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:b92ae920-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.110Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: timelion", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: timelion\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(*, metric=avg:bytes, split=ip:5)\",\"interval\":\"auto\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:e4d8b430-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.106Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-guage", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-guage\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"gauge\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_color_rules\":[{\"id\":\"e0be22e0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_width\":10,\"gauge_inner_width\":10,\"gauge_style\":\"half\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4c0f47e0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.111Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: markdown", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: markdown\",\"type\":\"markdown\",\"params\":{\"fontSize\":20,\"openLinksInNewTab\":false,\"markdown\":\"I'm a markdown!\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:2d1b1620-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_1_index_pattern", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_2_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.123Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1523481142694\",\"fieldName\":\"bytes\",\"parent\":\"\",\"label\":\"Bytes Input List\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1523481163654\",\"fieldName\":\"bytes\",\"parent\":\"\",\"label\":\"Bytes range\",\"type\":\"range\",\"options\":{\"decimalPlaces\":0,\"step\":1},\"indexPatternRefName\":\"control_1_index_pattern\"},{\"id\":\"1523481176519\",\"fieldName\":\"sound.keyword\",\"parent\":\"\",\"label\":\"Animal sounds\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_2_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.173Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"size.keyword\",\"value\":\"extra large\",\"params\":{\"query\":\"extra large\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"size.keyword\":{\"query\":\"extra large\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: non timebased line chart - dog data - with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"field\":\"trainability\"},\"schema\":\"metric\",\"type\":\"max\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"breed.keyword\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"field\":\"barking level\"},\"schema\":\"metric\",\"type\":\"max\"},{\"enabled\":true,\"id\":\"4\",\"params\":{\"field\":\"activity level\"},\"schema\":\"metric\",\"type\":\"max\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Max trainability\"},\"drawLinesBetweenPoints\":true,\"mode\":\"normal\",\"show\":\"true\",\"showCircles\":true,\"type\":\"line\",\"valueAxis\":\"ValueAxis-1\"},{\"data\":{\"id\":\"3\",\"label\":\"Max barking level\"},\"drawLinesBetweenPoints\":true,\"mode\":\"normal\",\"show\":true,\"showCircles\":true,\"type\":\"line\",\"valueAxis\":\"ValueAxis-1\"},{\"data\":{\"id\":\"4\",\"label\":\"Max activity level\"},\"drawLinesBetweenPoints\":true,\"mode\":\"normal\",\"show\":true,\"showCircles\":true,\"type\":\"line\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"line\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Max trainability\"},\"type\":\"value\"}]},\"title\":\"Rendering Test: non timebased line chart - dog data - with filter\",\"type\":\"line\"}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:42535e30-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.124Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: input control parent", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: input control parent\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1523481216736\",\"fieldName\":\"animal.keyword\",\"parent\":\"\",\"label\":\"Animal type\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1523481176519\",\"fieldName\":\"sound.keyword\",\"parent\":\"1523481216736\",\"label\":\"Animal sounds\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:7fda8ee0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.344Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: vega", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: vega\",\"type\":\"vega\",\"params\":{\"spec\":\"{\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v2.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: _all\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 0\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:02a2e4e0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.351Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-table", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-table\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"table\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_color_rules\":[{\"id\":\"e0be22e0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_width\":10,\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"markdown\":\"\\nHi Avg last bytes: {{ average_of_bytes.last.raw }}\",\"pivot_id\":\"bytes\",\"pivot_label\":\"Hello\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:f81134a0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.355Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-markdown", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-markdown\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"markdown\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_color_rules\":[{\"id\":\"e0be22e0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_width\":10,\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"markdown\":\"\\nHi Avg last bytes: {{ average_of_bytes.last.raw }}\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:df815d20-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.349Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-topn", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-topn\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"top_n\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}]},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:cc43fab0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.353Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-metric", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-metric\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"metric\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum_of_squares\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}]},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:c40f4d40-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.347Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-ts", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-ts\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:ffa2e0c0-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.153Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: goal", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: goal\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":4000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":2,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:37a541c0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.156Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"range\",\"key\":\"bytes\",\"value\":\"0 to 10,000\",\"params\":{\"gte\":0,\"lt\":10000},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"range\":{\"bytes\":{\"gte\":0,\"lt\":10000}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: geo map", + "uiStateJSON": "{\"mapZoom\":4,\"mapCenter\":[35.460669951495305,-85.60546875000001]}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: geo map\",\"type\":\"tile_map\",\"params\":{\"mapType\":\"Scaled Circle Markers\",\"isDesaturated\":true,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

© OpenStreetMap contributors | Elastic Maps Service

\",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

© OpenStreetMap contributors | Elastic Maps Service

\",\"subdomains\":[]}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"geohash_grid\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.coordinates\",\"autoPrecision\":true,\"isFilteredByCollar\":true,\"useGeocentroid\":true,\"precision\":3}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.162Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: datatable", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: datatable\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"clientip\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:3525b840-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.163Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: bar", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: bar\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":3,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:e6140540-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.165Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"CN\",\"params\":{\"query\":\"CN\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"CN\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: area with not filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: area with not filter\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"filters\",\"schema\":\"group\",\"params\":{\"filters\":[{\"input\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"label\":\"\"},{\"input\":{\"query\":\"bytes:>10\",\"language\":\"lucene\"}}]}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4c0c3f90-3e5a-11e8-9fc3-39e49624228e", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.166Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"field\":\"isDog\",\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"isDog\",\"value\":\"true\",\"params\":{\"value\":true},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"script\":{\"script\":{\"inline\":\"boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { return doc['animal.keyword'].value == 'dog' }, params.value);\",\"lang\":\"painless\",\"params\":{\"value\":true}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"weightLbs:>40\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: scripted filter and query", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: scripted filter and query\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"sound.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:34.195Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: animal sounds pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: animal sounds pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"sound.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:771b4f10-3e59-11e8-9fc3-39e49624228e", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:34.200Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + }, + "savedSearchRefName": "search_0", + "title": "Rendering Test: animal weights linked to search", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: animal weights linked to search\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"name.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:76c7f020-4194-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:34.583Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb top n with bytes filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb top n with bytes filter\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"top_n\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"Filter Bytes Test:>100\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"split_color_mode\":\"gradient\"},{\"id\":\"4fd5b150-4194-11e8-a461-7d278185cba4\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"4fd5b151-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"Filter Bytes Test:>3000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}]},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:0ca8c600-4195-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_0_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.229Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: input control with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: input control with filter\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1523896850250\",\"fieldName\":\"bytes\",\"parent\":\"\",\"label\":\"Byte Options\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":10,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:039e4770-4194-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.220Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb time series with bytes filter split by clientip", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb time series with bytes filter split by clientip\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:760a9060-4190-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.235Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"US\",\"params\":{\"query\":\"US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: max bytes in US - area chart with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: max bytes in US - area chart with filter\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Max bytes\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Max bytes\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:b3e70d00-4190-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.236Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: standard deviation heatmap with other bucket", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"-4,000 - 1,000\":\"rgb(247,252,245)\",\"1,000 - 6,000\":\"rgb(199,233,192)\",\"6,000 - 11,000\":\"rgb(116,196,118)\",\"11,000 - 16,000\":\"rgb(35,139,69)\"}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: standard deviation heatmap with other bucket\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Greens\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":false,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"std_dev\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":true,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"_term\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:c10c6b00-4191-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.267Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: max bytes guage percent mode", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 1\":\"rgb(0,104,55)\",\"1 - 15\":\"rgb(255,255,190)\",\"15 - 100\":\"rgb(165,0,38)\"}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: max bytes guage percent mode\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"extendRange\":true,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":500},{\"from\":500,\"to\":7500},{\"from\":7500,\"to\":50000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"Im subtext\",\"fontSize\":60,\"labelColor\":true},\"alignment\":\"horizontal\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:03d2afd0-4192-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.269Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: Goal unique count", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 10000\":\"rgb(0,104,55)\"}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: Goal unique count\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:7ff2c4c0-4191-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.270Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: Data table top hit with significant terms geo.src", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: Data table top hit with significant terms geo.src\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"top_hits\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\",\"aggregate\":\"average\",\"size\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"significant_terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"geo.src\",\"size\":10}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:df72ad40-4194-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.276Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"bytes\",\"value\":\"0\",\"params\":{\"query\":0,\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"bytes\":{\"query\":0,\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: tag cloud with not 0 bytes filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tag cloud with not 0 bytes filter\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:63983430-4192-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.275Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"US\",\"params\":{\"query\":\"US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"Filter Bytes Test:>5000\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: geo map with filter and query bytes > 5000 in US geo.src, heatmap setting", + "uiStateJSON": "{\"mapZoom\":7,\"mapCenter\":[42.98857645832184,-75.49804687500001]}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: geo map with filter and query bytes > 5000 in US geo.src, heatmap setting\",\"type\":\"tile_map\",\"params\":{\"mapType\":\"Heatmap\",\"isDesaturated\":true,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

© OpenStreetMap contributors | Elastic Maps Service

\",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

© OpenStreetMap contributors | Elastic Maps Service

\",\"subdomains\":[]}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"geohash_grid\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.coordinates\",\"autoPrecision\":true,\"isFilteredByCollar\":true,\"useGeocentroid\":true,\"precision\":4}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "search:be5accf0-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "agent", + "bytes", + "clientip" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Rendering Test: saved search", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-17T15:09:39.805Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "search:ca5ada40-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "agent", + "bytes", + "clientip" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[{\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"bytes\",\"value\":\"1,607\",\"params\":{\"query\":1607,\"type\":\"phrase\"},\"disabled\":false,\"alias\":null,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"bytes\":{\"query\":1607,\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Filter Bytes Test: search with filter", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-17T15:09:55.976Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:9bebe980-4192-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:59:42.648Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: timelion split 5 on bytes", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: timelion split 5 on bytes\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(*, split=bytes:5)\",\"interval\":\"auto\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:1dcdfe30-4192-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:59:56.976Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"bytes:>100\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: min bytes metric with query", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: min bytes metric with query\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"min\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:35417e50-4194-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T16:06:03.785Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb metric with custom interval and bytes filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb metric with custom interval and bytes filter\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"metric\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":1,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1d\",\"value_template\":\"{{value}} custom template\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}]},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:9fb4c670-4194-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T16:32:59.086Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb markdown", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb markdown\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"markdown\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"label\":\"\",\"var_name\":\"\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"markdown\":\"{{bytes_1000.last.formatted}}\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:befdb6b0-3e59-11e8-9fc3-39e49624228e", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T17:16:27.743Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal.keyword\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal.keyword\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"language\":\"lucene\",\"query\":\"\"}}" + }, + "savedSearchRefName": "search_0", + "title": "Filter Test: animals: linked to search with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Test: animals: linked to search with filter\",\"type\":\"pie\",\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"labels\":{\"last_level\":true,\"show\":false,\"truncate\":100,\"values\":true},\"legendPosition\":\"right\",\"type\":\"pie\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"name.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:584c0300-4191-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T18:36:30.315Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"bytes:>9000\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: split by geo with query", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: split by geo with query\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "config:7.0.0-alpha1", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": null, + "dateFormat:tz": "UTC", + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "notifications:lifetime:banner": 3600000, + "notifications:lifetime:error": 3600000, + "notifications:lifetime:info": 3600000, + "notifications:lifetime:warning": 3600000 + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-17T19:25:03.632Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:8090dcb0-4195-11e8-bb13-d53698fb349a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T19:28:21.967Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: vega", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: vega\",\"type\":\"vega\",\"params\":{\"spec\":\"{ \\nconfig: { kibana: { renderer: \\\"svg\\\" }},\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v2.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: _all\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 0\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"aggs\":[]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.2.4", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 16627, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "xPackMonitoring:showBanner": false + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-05-09T20:50:57.021Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:edb65990-53ca-11e8-b481-c9426d020fcd", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-05-09T20:52:47.144Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "table created in 6_2", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"table created in 6_2\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"weightLbs\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"animal.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:0644f890-53cb-11e8-b481-c9426d020fcd", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-05-09T20:53:28.345Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"weightLbs:>10\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Weight in lbs pie created in 6.2", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Weight in lbs pie created in 6.2\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"weightLbs\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:1b2f47b0-53cb-11e8-b481-c9426d020fcd", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:>15\"},\"filter\":[{\"meta\":{\"field\":\"isDog\",\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"isDog\",\"value\":\"true\",\"params\":{\"value\":true},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"script\":{\"script\":{\"inline\":\"boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { return doc['animal.keyword'].value == 'dog' }, params.value);\",\"lang\":\"painless\",\"params\":{\"value\":true}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":12,\"x\":24,\"y\":0,\"i\":\"4\"},\"panelIndex\":\"4\",\"version\":\"7.3.0\",\"panelRefName\":\"panel_0\",\"embeddableConfig\":{}},{\"gridData\":{\"w\":24,\"h\":12,\"x\":0,\"y\":0,\"i\":\"5\"},\"version\":\"7.3.0\",\"panelIndex\":\"5\",\"panelRefName\":\"panel_1\",\"embeddableConfig\":{}}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "Animal Weights (created in 6.2)", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "edb65990-53ca-11e8-b481-c9426d020fcd", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "0644f890-53cb-11e8-b481-c9426d020fcd", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-05-09T20:54:03.435Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fieldFormatMap": "{\"weightLbs\":{\"id\":\"number\",\"params\":{\"pattern\":\"0,0.0\"}},\"is_dog\":{\"id\":\"boolean\"},\"isDog\":{\"id\":\"boolean\"}}", + "fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"animal\",\"type\":\"string\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"animal.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"name\",\"type\":\"string\",\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"name.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sound\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"sound.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"weightLbs\",\"type\":\"number\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"isDog\",\"type\":\"boolean\",\"count\":0,\"scripted\":true,\"script\":\"return doc['animal.keyword'].value == 'dog'\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", + "timeFieldName": "@timestamp", + "title": "animals-*" + }, + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-05-09T20:55:44.314Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "search:6351c590-53cb-11e8-b481-c9426d020fcd", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "animal", + "sound", + "weightLbs" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:>10\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"sound.keyword\",\"value\":\"growl\",\"params\":{\"query\":\"growl\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"sound.keyword\":{\"query\":\"growl\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Search created in 6.2", + "version": 1 + }, + "type": "search", + "updated_at": "2018-05-09T20:56:04.457Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:47b5cf60-9e93-11ea-853e-adc0effaf76d", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "1b1789d0-9e93-11ea-853e-adc0effaf76d", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-05-25T15:16:27.743Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "vis with missing index pattern", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"title\":\"vis with missing index pattern\"}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:502e63a0-9e93-11ea-853e-adc0effaf76d", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"6cfbe6cc-1872-4cb4-9455-a02eeb75127e\"},\"panelIndex\":\"6cfbe6cc-1872-4cb4-9455-a02eeb75127e\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"}]", + "timeRestore": false, + "title": "dashboard with missing index pattern", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "47b5cf60-9e93-11ea-853e-adc0effaf76d", + "name": "panel_0", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-05-25T15:16:27.743Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:d9ce6000-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.2313, + "numberOfClicks": 0, + "timestamp": "2020-05-31T11:21:19.616Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:21:19.616Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:dc1d0af0-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 0.06526666666666667, + "numberOfClicks": 3, + "timestamp": "2020-05-31T11:21:23.487Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:21:23.487Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:f22c7920-a330-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 0.5732333333333333, + "numberOfClicks": 13, + "timestamp": "2020-05-31T11:22:00.498Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-05-31T11:22:00.498Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6eb8a840-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\n \"query\": {\n \"language\": \"kuery\",\n \"query\": \"\"\n },\n \"filter\": [\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": true,\n \"type\": \"phrase\",\n \"key\": \"name\",\n \"params\": {\n \"query\": \"moo\"\n },\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"\n },\n \"query\": {\n \"match_phrase\": {\n \"name\": \"moo\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": true,\n \"type\": \"phrase\",\n \"key\": \"baad-field\",\n \"params\": {\n \"query\": \"moo\"\n },\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index\"\n },\n \"query\": {\n \"match_phrase\": {\n \"baad-field\": \"moo\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": false,\n \"type\": \"phrase\",\n \"key\": \"@timestamp\",\n \"params\": {\n \"query\": \"123\"\n },\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[2].meta.index\"\n },\n \"query\": {\n \"match_phrase\": {\n \"@timestamp\": \"123\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": false,\n \"type\": \"exists\",\n \"key\": \"extension\",\n \"value\": \"exists\",\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[3].meta.index\"\n },\n \"exists\": {\n \"field\": \"extension\"\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": false,\n \"type\": \"phrase\",\n \"key\": \"banana\",\n \"params\": {\n \"query\": \"yellow\"\n }\n },\n \"query\": {\n \"match_phrase\": {\n \"banana\": \"yellow\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n }\n ]\n}" + }, + "optionsJSON": "{\n \"hidePanelTitles\": false,\n \"useMargins\": true\n}", + "panelsJSON": "[\n {\n \"version\": \"8.0.0\",\n \"gridData\": {\n \"x\": 0,\n \"y\": 0,\n \"w\": 24,\n \"h\": 15,\n \"i\": \"94a3dc1d-508a-4d42-a480-65b158925ba0\"\n },\n \"panelIndex\": \"94a3dc1d-508a-4d42-a480-65b158925ba0\",\n \"embeddableConfig\": {},\n \"panelRefName\": \"panel_0\"\n }\n]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "now-10y", + "timeRestore": true, + "timeTo": "now", + "title": "dashboard with bad filters", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-bad-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[2].meta.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[3].meta.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[4].meta.index", + "type": "index-pattern" + }, + { + "id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "name": "panel_0", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-06-04T09:26:04.272Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": null, + "dateFormat:tz": "UTC", + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "references": [ + ], + "type": "config", + "updated_at": "2020-06-04T09:22:54.572Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:b2a73c00-a645-11ea-b4c2-47e842e5fce5", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 1.2560333333333333, + "numberOfClicks": 19, + "timestamp": "2020-06-04T09:28:06.848Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-04T09:28:06.848Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:DashboardPanelVersionInUrl:8.0.0", + "index": ".kibana_1", + "source": { + "references": [ + ], + "type": "ui-metric", + "ui-metric": { + "count": 15 + }, + "updated_at": "2020-06-04T09:28:06.848Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6847ed80-a645-11ea-b4c2-47e842e5fce5", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 2.291733333333333, + "numberOfClicks": 16, + "timestamp": "2020-06-04T09:26:02.071Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-04T09:26:02.072Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:7b331140-a645-11ea-b4c2-47e842e5fce5", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 1.2560333333333333, + "numberOfClicks": 19, + "timestamp": "2020-06-04T09:26:33.812Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-04T09:26:33.812Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:kibana-user_agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36", + "index": ".kibana_1", + "source": { + "references": [ + ], + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-04T09:28:06.848Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:6f47f610-a646-11ea-b4c2-47e842e5fce5", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "dashboards", + "minutesOnScreen": 6.77335, + "numberOfClicks": 5, + "timestamp": "2020-06-04T09:33:23.313Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-04T09:33:23.313Z" + } + } +} \ No newline at end of file diff --git a/test/new_visualize_flow/fixtures/es_archiver/logstash_functional/data.json.gz b/test/new_visualize_flow/fixtures/es_archiver/logstash_functional/data.json.gz new file mode 100644 index 00000000000000..a4f889da61128a Binary files /dev/null and b/test/new_visualize_flow/fixtures/es_archiver/logstash_functional/data.json.gz differ diff --git a/test/new_visualize_flow/fixtures/es_archiver/logstash_functional/mappings.json b/test/new_visualize_flow/fixtures/es_archiver/logstash_functional/mappings.json new file mode 100644 index 00000000000000..010abff9cf6a9f --- /dev/null +++ b/test/new_visualize_flow/fixtures/es_archiver/logstash_functional/mappings.json @@ -0,0 +1,1118 @@ +{ + "type": "index", + "value": { + "index": "logstash-2015.09.22", + "mappings": { + "dynamic_templates": [ + { + "string_fields": { + "mapping": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "match": "*", + "match_mapping_type": "string" + } + } + ], + "properties": { + "@message": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "@tags": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "@timestamp": { + "type": "date" + }, + "agent": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "bytes": { + "type": "long" + }, + "clientip": { + "type": "ip" + }, + "extension": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + }, + "dest": { + "type": "keyword" + }, + "src": { + "type": "keyword" + }, + "srcdest": { + "type": "keyword" + } + } + }, + "headings": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "host": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "type": "integer" + }, + "index": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "ip": { + "type": "ip" + }, + "links": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "machine": { + "properties": { + "os": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "ram": { + "type": "long" + } + } + }, + "memory": { + "type": "double" + }, + "meta": { + "properties": { + "char": { + "type": "keyword" + }, + "related": { + "type": "text" + }, + "user": { + "properties": { + "firstname": { + "type": "text" + }, + "lastname": { + "type": "integer" + } + } + } + } + }, + "nestedField": { + "type": "nested", + "properties": { + "child": { + "type": "keyword" + } + } + }, + "phpmemory": { + "type": "long" + }, + "referer": { + "type": "keyword" + }, + "relatedContent": { + "properties": { + "article:modified_time": { + "type": "date" + }, + "article:published_time": { + "type": "date" + }, + "article:section": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "article:tag": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:description": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image:height": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image:width": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:site_name": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:title": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:type": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:card": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:description": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:image": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:site": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:title": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "request": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "response": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "spaces": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + }, + "url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "utc_time": { + "type": "date" + }, + "xss": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "settings": { + "index": { + "analysis": { + "analyzer": { + "url": { + "max_token_length": "1000", + "tokenizer": "uax_url_email", + "type": "standard" + } + } + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": "logstash-2015.09.20", + "mappings": { + "dynamic_templates": [ + { + "string_fields": { + "mapping": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "match": "*", + "match_mapping_type": "string" + } + } + ], + "properties": { + "@message": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "@tags": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "@timestamp": { + "type": "date" + }, + "agent": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "bytes": { + "type": "long" + }, + "clientip": { + "type": "ip" + }, + "extension": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + }, + "dest": { + "type": "keyword" + }, + "src": { + "type": "keyword" + }, + "srcdest": { + "type": "keyword" + } + } + }, + "headings": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "host": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "type": "integer" + }, + "index": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "ip": { + "type": "ip" + }, + "links": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "machine": { + "properties": { + "os": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "ram": { + "type": "long" + } + } + }, + "memory": { + "type": "double" + }, + "meta": { + "properties": { + "char": { + "type": "keyword" + }, + "related": { + "type": "text" + }, + "user": { + "properties": { + "firstname": { + "type": "text" + }, + "lastname": { + "type": "integer" + } + } + } + } + }, + "nestedField": { + "type": "nested", + "properties": { + "child": { + "type": "keyword" + } + } + }, + "phpmemory": { + "type": "long" + }, + "referer": { + "type": "keyword" + }, + "relatedContent": { + "properties": { + "article:modified_time": { + "type": "date" + }, + "article:published_time": { + "type": "date" + }, + "article:section": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "article:tag": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:description": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image:height": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image:width": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:site_name": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:title": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:type": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:card": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:description": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:image": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:site": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:title": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "request": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "response": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "spaces": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + }, + "url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "utc_time": { + "type": "date" + }, + "xss": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "settings": { + "index": { + "analysis": { + "analyzer": { + "url": { + "max_token_length": "1000", + "tokenizer": "uax_url_email", + "type": "standard" + } + } + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": "logstash-2015.09.21", + "mappings": { + "dynamic_templates": [ + { + "string_fields": { + "mapping": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "match": "*", + "match_mapping_type": "string" + } + } + ], + "properties": { + "@message": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "@tags": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "@timestamp": { + "type": "date" + }, + "agent": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "bytes": { + "type": "long" + }, + "clientip": { + "type": "ip" + }, + "extension": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + }, + "dest": { + "type": "keyword" + }, + "src": { + "type": "keyword" + }, + "srcdest": { + "type": "keyword" + } + } + }, + "headings": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "host": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "type": "integer" + }, + "index": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "ip": { + "type": "ip" + }, + "links": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "machine": { + "properties": { + "os": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "ram": { + "type": "long" + } + } + }, + "memory": { + "type": "double" + }, + "meta": { + "properties": { + "char": { + "type": "keyword" + }, + "related": { + "type": "text" + }, + "user": { + "properties": { + "firstname": { + "type": "text" + }, + "lastname": { + "type": "integer" + } + } + } + } + }, + "nestedField": { + "type": "nested", + "properties": { + "child": { + "type": "keyword" + } + } + }, + "phpmemory": { + "type": "long" + }, + "referer": { + "type": "keyword" + }, + "relatedContent": { + "properties": { + "article:modified_time": { + "type": "date" + }, + "article:published_time": { + "type": "date" + }, + "article:section": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "article:tag": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:description": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image:height": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:image:width": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:site_name": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:title": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:type": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "og:url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:card": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:description": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:image": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:site": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "twitter:title": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "request": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "response": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "spaces": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + }, + "url": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + }, + "utc_time": { + "type": "date" + }, + "xss": { + "fields": { + "raw": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "settings": { + "index": { + "analysis": { + "analyzer": { + "url": { + "max_token_length": "1000", + "tokenizer": "uax_url_email", + "type": "standard" + } + } + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/test/new_visualize_flow/index.ts b/test/new_visualize_flow/index.ts index e915525155990e..08c00a0074bef7 100644 --- a/test/new_visualize_flow/index.ts +++ b/test/new_visualize_flow/index.ts @@ -19,9 +19,14 @@ import { FtrProviderContext } from '../functional/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function ({ loadTestFile }: FtrProviderContext) { +export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('New Visualize Flow', function () { this.tags('ciGroup2'); + const esArchiver = getService('esArchiver'); + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + loadTestFile(require.resolve('./dashboard_embedding')); }); } diff --git a/x-pack/dev-tools/api_debug/apis/telemetry/index.js b/x-pack/dev-tools/api_debug/apis/telemetry/index.js index 5cee4cdf45d6d5..9387e3101bd197 100644 --- a/x-pack/dev-tools/api_debug/apis/telemetry/index.js +++ b/x-pack/dev-tools/api_debug/apis/telemetry/index.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; - export const name = 'telemetry'; export const description = 'Get the clusters stats from the Kibana server'; export const method = 'POST'; export const path = '/api/telemetry/v2/clusters/_stats'; -export const body = { timeRange: moment().valueOf(), unencrypted: true }; +export const body = { unencrypted: true }; 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 abbe1d2a48d113..6e44479d058d8e 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 @@ -64,10 +64,9 @@ const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = { large: 10000, }; -export const AlwaysFiringExpression: React.FunctionComponent> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => { +export const AlwaysFiringExpression: React.FunctionComponent< + AlertTypeParamsExpressionProps +> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => { const { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId), diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 2735a1ab445212..0d41b520501add 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -155,9 +155,11 @@ export class ActionsClient { 'update' ); } - const { attributes, references, version } = await this.unsecuredSavedObjectsClient.get< - RawAction - >('action', id); + const { + attributes, + references, + version, + } = await this.unsecuredSavedObjectsClient.get('action', id); const { actionTypeId } = attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index a19a662f8323cb..b5676fc837fe8f 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -64,9 +64,9 @@ describe('ensureAuthorized', () => { test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ request, @@ -105,9 +105,9 @@ describe('ensureAuthorized', () => { test('ensures the user has privileges to execute an Actions Saved Object type', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ request, @@ -156,9 +156,9 @@ describe('ensureAuthorized', () => { test('throws if user lacks the required privieleges', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ request, @@ -198,9 +198,9 @@ describe('ensureAuthorized', () => { test('exempts users from requiring privileges to execute actions when authorizationMode is Legacy', async () => { const { authorization, authentication } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ request, diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a160735e89a935..e61936321b8e0a 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 0e6c2ff37eb029..39a61cebe92dcd 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 fac57b6282c445..f86c6a40e05055 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/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index c08ff9449d1513..c83e24c5a45f4f 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -454,9 +454,11 @@ export class AlertsClient { let attributes: RawAlert; try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; attributes = decryptedAlert.attributes; @@ -505,9 +507,11 @@ export class AlertsClient { let alertSavedObject: SavedObject; try { - alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); } catch (e) { // We'll skip invalidating the API key since we failed to load the decrypted saved object this.logger.error( @@ -636,9 +640,11 @@ export class AlertsClient { let version: string | undefined; try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; attributes = decryptedAlert.attributes; version = decryptedAlert.version; @@ -707,9 +713,11 @@ export class AlertsClient { let version: string | undefined; try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; attributes = decryptedAlert.attributes; version = decryptedAlert.version; @@ -789,9 +797,11 @@ export class AlertsClient { let version: string | undefined; try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; attributes = decryptedAlert.attributes; version = decryptedAlert.version; diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 6d259029ac4806..171ed13763c46b 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -378,9 +378,7 @@ describe('create()', () => { "scheduledTaskId": "task-123", } `); - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< - ActionsClient - >; + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); }); @@ -702,9 +700,7 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); // Reset from default behaviour - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< - ActionsClient - >; + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; actionsClient.getBulk.mockReset(); actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index d0bb2607f7a47a..046d7ec63c048c 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -328,9 +328,7 @@ describe('update()', () => { "version": "123", } `); - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< - ActionsClient - >; + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index bdbfc726dab8f6..49a90c62bc5819 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -68,9 +68,9 @@ const actionsAuthorization = actionsAuthorizationMock.create(); beforeEach(() => { jest.resetAllMocks(); alertsClientFactoryParams.actions = actionsMock.createStart(); - (alertsClientFactoryParams.actions as jest.Mocked< - ActionsStartContract - >).getActionsAuthorizationWithRequest.mockReturnValue(actionsAuthorization); + (alertsClientFactoryParams.actions as jest.Mocked).getActionsAuthorizationWithRequest.mockReturnValue( + actionsAuthorization + ); alertsClientFactoryParams.getSpaceId.mockReturnValue('default'); alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default'); }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index b3c7ada26c4569..eb116b9e208dc2 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -240,9 +240,9 @@ describe('AlertsAuthorization', () => { test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const alertAuthorization = new AlertsAuthorization({ request, @@ -283,9 +283,9 @@ describe('AlertsAuthorization', () => { test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const alertAuthorization = new AlertsAuthorization({ request, @@ -326,9 +326,9 @@ describe('AlertsAuthorization', () => { test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -377,9 +377,9 @@ describe('AlertsAuthorization', () => { test('throws if user lacks the required privieleges for the consumer', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const alertAuthorization = new AlertsAuthorization({ request, @@ -428,9 +428,9 @@ describe('AlertsAuthorization', () => { test('throws if user lacks the required privieleges for the producer', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const alertAuthorization = new AlertsAuthorization({ request, @@ -479,9 +479,9 @@ describe('AlertsAuthorization', () => { test('throws if user lacks the required privieleges for both consumer and producer', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const alertAuthorization = new AlertsAuthorization({ request, @@ -594,9 +594,9 @@ describe('AlertsAuthorization', () => { test('creates a filter based on the privileged types', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -625,9 +625,9 @@ describe('AlertsAuthorization', () => { test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -686,9 +686,9 @@ describe('AlertsAuthorization', () => { test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -736,9 +736,9 @@ describe('AlertsAuthorization', () => { test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -913,9 +913,9 @@ describe('AlertsAuthorization', () => { test('augments a list of types with consumers under which the operation is authorized', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -1001,9 +1001,9 @@ describe('AlertsAuthorization', () => { test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -1062,9 +1062,9 @@ describe('AlertsAuthorization', () => { test('augments a list of types with consumers under which multiple operations are authorized', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -1174,9 +1174,9 @@ describe('AlertsAuthorization', () => { test('omits types which have no consumers under which the operation is authorized', async () => { const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts index 77cbb9f4a4a85c..119c3b697fd2ee 100644 --- a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -202,9 +202,10 @@ async function invalidateApiKeys( let totalInvalidated = 0; await Promise.all( apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { - const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser< - InvalidatePendingApiKey - >('api_key_pending_invalidation', apiKeyObj.id); + const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'api_key_pending_invalidation', + apiKeyObj.id + ); const apiKeyId = decryptedApiKey.attributes.apiKeyId; const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup); if (response.apiKeysEnabled === true && response.result.error_count > 0) { diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 99cb45130718ab..4bfb44425544a9 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/saved_objects/partially_update_alert.test.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts index 8041ec551bb0df..c272e0490cbab6 100644 --- a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts @@ -14,9 +14,7 @@ import { partiallyUpdateAlert, PartiallyUpdateableAlertAttributes } from './part import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; const MockSavedObjectsClientContract = savedObjectsClientMock.create(); -const MockISavedObjectsRepository = (MockSavedObjectsClientContract as unknown) as jest.Mocked< - ISavedObjectsRepository ->; +const MockISavedObjectsRepository = (MockSavedObjectsClientContract as unknown) as jest.Mocked; describe('partially_update_alert', () => { beforeEach(() => { 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 a5f83bc393d4ec..e731e3f536261f 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 de82dd31877afb..40a9983ae27861 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/jest.config.js b/x-pack/plugins/apm/jest.config.js index ffd3a39e8afd1a..849dd7f5c3e2df 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/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index e08bd01a1842be..e6fc80ed7c3b7f 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -23,9 +23,7 @@ import { useUrlParams } from '../../hooks/useUrlParams'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { units } from '../../style/variables'; -export type AnomalyDetectionApiResponse = APIReturnType< - 'GET /api/apm/settings/anomaly-detection' ->; +export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection'>; const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index dfc3d6b4b9ec8d..7fcbe7c518cd0b 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/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 777ee014d3e58b..643064b2f31764 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 @@ -55,9 +55,7 @@ const TransactionLinkName = styled.div` `; interface Props { - errorGroup: APIReturnType< - 'GET /api/apm/services/{serviceName}/errors/{groupId}' - >; + 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 fd656b8be6ec72..99316e3520a763 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 @@ -22,9 +22,7 @@ import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/useTheme'; -type ErrorDistributionAPIResponse = APIReturnType< - 'GET /api/apm/services/{serviceName}/errors/distribution' ->; +type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>; interface FormattedBucket { x0: 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 bfa426985d1c66..be1078ea860c30 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 @@ -48,9 +48,7 @@ const Culprit = styled.div` font-family: ${fontFamilyCode}; `; -type ErrorGroupListAPIResponse = APIReturnType< - 'GET /api/apm/services/{serviceName}/errors' ->; +type ErrorGroupListAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors'>; interface Props { items: ErrorGroupListAPIResponse; 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 f96dc14e342645..63fb69d6d7cbfb 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/PageLoadDistribution/BreakdownSeries.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx index f348aca495c71d..147f2e495066a1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx @@ -6,6 +6,7 @@ import { CurveType, Fit, LineSeries, ScaleType } from '@elastic/charts'; import React, { useEffect } from 'react'; +import numeral from '@elastic/numeral'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT, @@ -63,6 +64,7 @@ export function BreakdownSeries({ sortIndex === 0 ? 0 : sortIndex + 1 ] } + tickFormat={(d) => numeral(d).format('0.0') + ' %'} /> ))} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index b757635af1702e..3a5c3d80ca7d19 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -61,9 +61,9 @@ export function EmbeddedMapComponent() { MapEmbeddable | ErrorEmbeddable | undefined >(); - const embeddableRoot: React.RefObject = useRef< - HTMLDivElement - >(null); + const embeddableRoot: React.RefObject = useRef( + null + ); const { services: { embeddable: embeddablePlugin }, 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 4610205cee7ed0..7ce9d3f25354cc 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/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 89c5c801a56836..74e7b652d0ebe0 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -45,7 +45,9 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { const { uiFilters, urlParams } = useUrlParams(); const { start, end } = urlParams; - const localFiltersConfig: React.ComponentProps = useMemo( + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( () => ({ filterNames: ['host', 'containerId', 'podName'], params: { 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 5f8e0b9052a656..4af9321152da35 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 9687846d6c5205..c6566af3a8b61b 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 3a2aa01ba3bc48..7fa8e3a025956d 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 2017aa42e1c5a6..00000000000000 --- 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 a7feafad11111a..96a634828f6696 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 d872f6d21ed96d..771a8c6154dc04 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/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index debf3fa85d9353..2cda5fcf859099 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -17,9 +17,7 @@ import { LicensePrompt } from '../../../shared/LicensePrompt'; import { useLicense } from '../../../../hooks/useLicense'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -export type AnomalyDetectionApiResponse = APIReturnType< - 'GET /api/apm/settings/anomaly-detection' ->; +export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection'>; const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], 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 ac4af7b126468c..e92a6c7db8445e 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 @@ -33,13 +33,9 @@ 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 TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; -type DistributionApiResponse = 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]; 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 86221a6e928531..c9420dbb81cb99 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 @@ -27,9 +27,7 @@ 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 DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; type DistributionBucket = DistributionApiResponse['buckets'][0]; 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 cc6bacc4f3ccb8..8a99773a97baf6 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'; 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 65dfdd19fa0c53..953397b9f3d5fb 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 @@ -10,9 +10,7 @@ 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]; +type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>['items'][0]; export default { title: 'app/TransactionOverview/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 b084d05ee16e8c..ece923631a2f76 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 @@ -20,9 +20,7 @@ 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]; +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. 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 8208916c203377..a55b135c6a84e2 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'; @@ -96,7 +96,9 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { status: transactionListStatus, } = useTransactionList(urlParams); - const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( () => ({ filterNames: [ 'transactionResult', 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 8df2b0fda7a7e1..70acc2038e1a77 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 f42b94b8afe335..22c5a2b101ddcb 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/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 3fa047d840ddad..3c84b3982642d4 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 @@ -99,7 +99,9 @@ export function ServiceInventory() { useTrackPageview({ app: 'apm', path: 'services_overview' }); useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); - const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( () => ({ filterNames: ['host', 'agentName'], projection: Projection.services, 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 78% 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 5808c54d578c6d..5dc1645a1760d0 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,10 +31,12 @@ 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( + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( () => ({ filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], params: { @@ -60,7 +62,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 80% 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 efa6110fea1008..59e919199be761 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 @@ -9,7 +9,7 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, + EuiPage, EuiPanel, EuiSpacer, EuiStat, @@ -22,15 +22,16 @@ 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'; +import { SearchBar } from '../../shared/search_bar'; const INITIAL_DATA = { host: '', @@ -42,6 +43,13 @@ const Truncate = styled.span` ${truncate(px(unit * 12))} `; +const MetadataFlexGroup = styled(EuiFlexGroup)` + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; + margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; + padding: ${({ theme }) => + `${theme.eui.paddingSizes.m} 0 0 ${theme.eui.paddingSizes.m}`}; +`; + type ServiceNodeMetricsProps = RouteComponentProps<{ serviceName: string; serviceNodeName: string; @@ -75,11 +83,10 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { ); const isLoading = status === FETCH_STATUS.LOADING; - const isAggregatedData = serviceNodeName === SERVICE_NODE_NAME_MISSING; return ( - <div> + <> <ApmHeader> <EuiFlexGroup alignItems="center"> <EuiFlexItem grow={false}> @@ -89,7 +96,6 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { </EuiFlexItem> </EuiFlexGroup> </ApmHeader> - <EuiHorizontalRule margin="m" /> {isAggregatedData ? ( <EuiCallOut title={i18n.translate( @@ -121,7 +127,7 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { /> </EuiCallOut> ) : ( - <EuiFlexGroup gutterSize="xl"> + <MetadataFlexGroup gutterSize="xl"> <EuiFlexItem grow={false}> <EuiStat titleSize="s" @@ -152,7 +158,7 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { } /> </EuiFlexItem> - <EuiFlexItem grow={false}> + <EuiFlexItem> <EuiStat titleSize="s" isLoading={isLoading} @@ -169,16 +175,20 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { } /> </EuiFlexItem> - </EuiFlexGroup> + </MetadataFlexGroup> )} - <EuiHorizontalRule margin="m" /> {agentName && ( <ChartsSyncContextProvider> <EuiFlexGrid columns={2} gutterSize="s"> {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> ))} @@ -186,6 +196,28 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { <EuiSpacer size="xxl" /> </ChartsSyncContextProvider> )} - </div> + <SearchBar /> + <EuiPage> + {agentName && ( + <ChartsSyncContextProvider> + <EuiFlexGrid columns={2} gutterSize="s"> + {data.charts.map((chart) => ( + <EuiFlexItem key={chart.key}> + <EuiPanel> + <MetricsChart + start={start} + end={end} + chart={chart} + fetchStatus={status} + /> + </EuiPanel> + </EuiFlexItem> + ))} + </EuiFlexGrid> + <EuiSpacer size="xxl" /> + </ChartsSyncContextProvider> + )} + </EuiPage> + </> ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index e91ab338c4a27b..e241bc2fed05a4 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -36,9 +36,7 @@ import { ImpactBar } from '../../../shared/ImpactBar'; import { ServiceOverviewTable } from '../service_overview_table'; type ServiceTransactionGroupItem = ValuesType< - APIReturnType< - 'GET /api/apm/services/{serviceName}/overview_transaction_groups' - >['transactionGroups'] + APIReturnType<'GET /api/apm/services/{serviceName}/overview_transaction_groups'>['transactionGroups'] >; interface Props { 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 62952d1fb501b7..00000000000000 --- 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 27c6aa82ac674b..00000000000000 --- 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 6b421bc3703322..00000000000000 --- 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 d6484f52e84f98..00000000000000 --- 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 88a4137b47200a..16d526bda2103a 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 00000000000000..0304b850d6ceec --- /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 29e93a47629b31..0241167aba1fb0 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 09cdaa26004bb5..36b370b4069aea 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 5abeae265dfa6b..db7a284f6adff6 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 00000000000000..2825363b101976 --- /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 f5a57544209f57..15a85113406e15 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 05cae589c19fc6..677e4b7593ff10 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 9fc16ab0f9eab9..00000000000000 --- 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 e70c53108cb0e5..00000000000000 --- 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 5aa315d599e18e..00000000000000 --- 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 2c4cc185dac7eb..00000000000000 --- 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 a4286578d44d16..00000000000000 --- 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 e49899da85e0d0..00000000000000 --- 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 51cb3c3885765e..00000000000000 --- 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 26b03672f1c1f3..00000000000000 --- 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 501d30b5e2ba12..00000000000000 --- 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 117ec26446de8c..00000000000000 --- 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 67b7fd31b05bc3..00000000000000 --- 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 9d127c06e0c144..00000000000000 --- 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 20636fa1444797..00000000000000 --- 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 e8b96b501af0f9..00000000000000 --- 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 7183c4851e9936..00000000000000 --- 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 935895022931c9..f45e207c32c8f9 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 d0301880ef52a0..ca328473db8ccc 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 2f63a77132be98..9a561571df5a7c 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 d96f3cd698aed7..6f1f4e01c4d1f6 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'; @@ -59,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 a5d146fcd73ec8..3819ed30d104a0 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,9 +4,7 @@ * 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'; 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 b40df89a22c33d..918e940651dee6 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 2a5948d0ebf0be..41212aa7b982c7 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 dd9a1e2ec2efe4..b9028ff2e9e8c8 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/context/charts_sync_context.tsx b/x-pack/plugins/apm/public/context/charts_sync_context.tsx index 282097fed2460d..d983a857a26ec1 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 78ea30f466cfa9..c790ac57edc3bf 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 9980569ee54dd1..9cbfee37d12530 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -13,9 +13,7 @@ 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' ->; +type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; const INITIAL_DATA = { buckets: [] as APIResponse['buckets'], diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index e847309fd02654..92b54beb715db7 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -10,9 +10,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { APIReturnType } from '../services/rest/createCallApmApi'; import { useFetcher } from './useFetcher'; -type TransactionsAPIResponse = APIReturnType< - 'GET /api/apm/services/{serviceName}/transaction_groups' ->; +type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>; const DEFAULT_RESPONSE: Partial<TransactionsAPIResponse> = { items: undefined, 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 52c7e4c1e3a31a..cde5c84a6097b2 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 901e6052bbf06c..4269ec0e6c0f36 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 450f02f70c6a42..8330df07c21eb0 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/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts index c1cb903a0bb3ef..0df1cbf0e0eef8 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 723ff03dc4995b..4739a5b621972c 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 ca47540b04d826..8c64c37d9b7f74 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/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 225afff2818abb..781209d10034de 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -15,6 +15,17 @@ export function microToSec(val: number) { return Math.round((val / MICRO_TO_SEC + Number.EPSILON) * 100) / 100; } +export function removeZeroesFromTail( + distData: Array<{ x: number; y: number }> +) { + if (distData.length > 0) { + while (distData[distData.length - 1].y === 0) { + distData.pop(); + } + } + return distData; +} + export const getPLDChartSteps = ({ maxDuration, minDuration, @@ -132,18 +143,14 @@ export async function getPageLoadDistribution({ } // calculate the diff to get actual page load on specific duration value - const pageDist = pageDistVals.map(({ key, value }, index: number, arr) => { + let pageDist = pageDistVals.map(({ key, value }, index: number, arr) => { return { x: microToSec(key), y: index === 0 ? value : value - arr[index - 1].value, }; }); - if (pageDist.length > 0) { - while (pageDist[pageDist.length - 1].y === 0) { - pageDist.pop(); - } - } + pageDist = removeZeroesFromTail(pageDist); Object.entries(durPercentiles?.values ?? {}).forEach(([key, val]) => { if (durPercentiles?.values?.[key]) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index e2ec59d232b211..f77165b6e76fdf 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -19,6 +19,7 @@ import { getPLDChartSteps, MICRO_TO_SEC, microToSec, + removeZeroesFromTail, } from './get_page_load_distribution'; export const getBreakdownField = (breakdown: string) => { @@ -95,14 +96,21 @@ export const getPageLoadDistBreakdown = async ({ const pageDistBreakdowns = aggregations?.breakdowns.buckets; return pageDistBreakdowns?.map(({ key, page_dist: pageDist }) => { - return { - name: String(key), - data: pageDist.values?.map(({ key: pKey, value }, index: number, arr) => { + let seriesData = pageDist.values?.map( + ({ key: pKey, value }, index: number, arr) => { return { x: microToSec(pKey), y: index === 0 ? value : value - arr[index - 1].value, }; - }), + } + ); + + // remove 0 values from tail + seriesData = removeZeroesFromTail(seriesData); + + return { + name: String(key), + data: seriesData, }; }); }; 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 14245ce1d6c833..bcd6d10d319878 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/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts index 393a73f7c1ccd2..b971a1c28397d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts @@ -10,9 +10,7 @@ import { ESResponse } from './fetcher'; type IBucket = ReturnType<typeof getBucket>; function getBucket( - bucket: Required< - ESResponse - >['aggregations']['ml_avg_response_times']['buckets'][0] + bucket: Required<ESResponse>['aggregations']['ml_avg_response_times']['buckets'][0] ) { return { x: bucket.key, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts index e5d6aad693869c..eecb3e7177ef6e 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts @@ -42,9 +42,7 @@ export function timeseriesTransformer({ }; } -type TransactionResultBuckets = Required< - ESResponse ->['aggregations']['transaction_results']['buckets']; +type TransactionResultBuckets = Required<ESResponse>['aggregations']['transaction_results']['buckets']; export function getTpmBuckets({ transactionResultBuckets = [], @@ -86,9 +84,7 @@ export function getTpmBuckets({ ); } -type ResponseTimeBuckets = Required< - ESResponse ->['aggregations']['response_times']['buckets']; +type ResponseTimeBuckets = Required<ESResponse>['aggregations']['response_times']['buckets']; function getResponseTime(responseTimeBuckets: ResponseTimeBuckets = []) { return responseTimeBuckets.reduce( diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.ts index a65d23d8c14060..cac60fea784d3b 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/update.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.ts @@ -37,9 +37,10 @@ export function initializeUpdateCustomElementRoute(deps: RouteInitializerDeps) { const now = new Date().toISOString(); - const customElementObject = await context.core.savedObjects.client.get< - CustomElementAttributes - >(CUSTOM_ELEMENT_TYPE, id); + const customElementObject = await context.core.savedObjects.client.get<CustomElementAttributes>( + CUSTOM_ELEMENT_TYPE, + id + ); await context.core.savedObjects.client.create<CustomElementAttributes>( CUSTOM_ELEMENT_TYPE, diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md index 002fbfb8b53f78..30011148cd1e7b 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 b4daac93940d88..920858a1e39b4a 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 50e104b30178ab..d00df5a3246bd2 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 a95b7833a5232f..169157c95d4c1a 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 e14281e047915a..90bb1d604e7330 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 aa503e96be30d3..039c0e2e7e67f1 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 b3a05163fa6f4d..da15f64a5718f5 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 9314ebb445820f..4c0b5887ca9988 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 400e8ca404ca5e..5cb411f17a7448 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 e75e89fa207b91..82fe3fce67653c 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 0b733bb034f8cc..2909aa40a44252 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 01de9abac16afe..ca6598fcb288c0 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 @@ -46,9 +46,9 @@ describe('GET case', () => { ); const response = await routeHandler(theContext, request, kibanaResponseFactory); - const savedObject = (mockCases.find((s) => s.id === 'mock-id-1') as unknown) as SavedObject< - ESCaseAttributes - >; + const savedObject = (mockCases.find( + (s) => s.id === 'mock-id-1' + ) as unknown) as SavedObject<ESCaseAttributes>; expect(response.status).toEqual(200); expect(response.payload).toEqual( flattenCaseSavedObject({ @@ -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 80b65b54468fcb..6ba2da111090f7 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 fc1086b03814b6..a67bae5ed74dc9 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 f8fe149c2ff2f6..589d7c02a7be60 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 87478eb23641f3..8f398c63e01bd5 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 cab8cb499c3fae..0ce2b196af471e 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/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx index b21081e10bbe1f..d97d10512783c7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx @@ -11,9 +11,8 @@ import { createConnectedBackgroundSessionIndicator } from './connected_backgroun import { BehaviorSubject } from 'rxjs'; import { ISessionService } from '../../../../../../../src/plugins/data/public'; -const sessionService = dataPluginMock.createStartContract().search.session as jest.Mocked< - ISessionService ->; +const sessionService = dataPluginMock.createStartContract().search + .session as jest.Mocked<ISessionService>; test("shouldn't show indicator in case no active search session", async () => { const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts index a82a162a14e2b3..f1e06a0cec03df 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts @@ -9,14 +9,16 @@ import { EncryptedSavedObjectAttributesDefinition } from './encrypted_saved_obje it('correctly determines attribute properties', () => { const attributes = ['attr#1', 'attr#2', 'attr#3', 'attr#4']; - const cases: Array<[ - EncryptedSavedObjectTypeRegistration, - { - shouldBeEncrypted: boolean[]; - shouldBeExcludedFromAAD: boolean[]; - shouldBeStripped: boolean[]; - } - ]> = [ + const cases: Array< + [ + EncryptedSavedObjectTypeRegistration, + { + shouldBeEncrypted: boolean[]; + shouldBeExcludedFromAAD: boolean[]; + shouldBeStripped: boolean[]; + } + ] + > = [ [ { type: 'so-type', @@ -111,3 +113,18 @@ it('correctly determines attribute properties', () => { } } }); + +it('it correctly sets allowPredefinedID', () => { + const defaultTypeDefinition = new EncryptedSavedObjectAttributesDefinition({ + type: 'so-type', + attributesToEncrypt: new Set(['attr#1', 'attr#2']), + }); + expect(defaultTypeDefinition.allowPredefinedID).toBe(false); + + const typeDefinitionWithPredefinedIDAllowed = new EncryptedSavedObjectAttributesDefinition({ + type: 'so-type', + attributesToEncrypt: new Set(['attr#1', 'attr#2']), + allowPredefinedID: true, + }); + expect(typeDefinitionWithPredefinedIDAllowed.allowPredefinedID).toBe(true); +}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts index 849a2888b6e1a5..398a64585411a7 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts @@ -15,6 +15,7 @@ export class EncryptedSavedObjectAttributesDefinition { public readonly attributesToEncrypt: ReadonlySet<string>; private readonly attributesToExcludeFromAAD: ReadonlySet<string> | undefined; private readonly attributesToStrip: ReadonlySet<string>; + public readonly allowPredefinedID: boolean; constructor(typeRegistration: EncryptedSavedObjectTypeRegistration) { const attributesToEncrypt = new Set<string>(); @@ -34,6 +35,7 @@ export class EncryptedSavedObjectAttributesDefinition { this.attributesToEncrypt = attributesToEncrypt; this.attributesToStrip = attributesToStrip; this.attributesToExcludeFromAAD = typeRegistration.attributesToExcludeFromAAD; + this.allowPredefinedID = !!typeRegistration.allowPredefinedID; } /** diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts index c692d8698771fe..0138e929ca1caf 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts @@ -13,6 +13,7 @@ import { function createEncryptedSavedObjectsServiceMock() { return ({ isRegistered: jest.fn(), + canSpecifyID: jest.fn(), stripOrDecryptAttributes: jest.fn(), encryptAttributes: jest.fn(), decryptAttributes: jest.fn(), @@ -52,6 +53,12 @@ export const encryptedSavedObjectsServiceMock = { mock.isRegistered.mockImplementation( (type) => registrations.findIndex((r) => r.type === type) >= 0 ); + mock.canSpecifyID.mockImplementation((type, version, overwrite) => { + const registration = registrations.find((r) => r.type === type); + return ( + registration === undefined || registration.allowPredefinedID || !!(version && overwrite) + ); + }); mock.encryptAttributes.mockImplementation(async (descriptor, attrs) => processAttributes( descriptor, diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index 88d57072697fe6..6bc4a392064e43 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -89,6 +89,45 @@ describe('#isRegistered', () => { }); }); +describe('#canSpecifyID', () => { + it('returns true for unknown types', () => { + expect(service.canSpecifyID('unknown-type')).toBe(true); + }); + + it('returns true for types registered setting allowPredefinedID to true', () => { + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attr-1']), + allowPredefinedID: true, + }); + expect(service.canSpecifyID('known-type-1')).toBe(true); + }); + + it('returns true when overwriting a saved object with a version specified even when allowPredefinedID is not set', () => { + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attr-1']), + }); + expect(service.canSpecifyID('known-type-1', '2', true)).toBe(true); + expect(service.canSpecifyID('known-type-1', '2', false)).toBe(false); + expect(service.canSpecifyID('known-type-1', undefined, true)).toBe(false); + }); + + it('returns false for types registered without setting allowPredefinedID', () => { + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attr-1']) }); + expect(service.canSpecifyID('known-type-1')).toBe(false); + }); + + it('returns false for types registered setting allowPredefinedID to false', () => { + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attr-1']), + allowPredefinedID: false, + }); + expect(service.canSpecifyID('known-type-1')).toBe(false); + }); +}); + describe('#stripOrDecryptAttributes', () => { it('does not strip attributes from unknown types', async () => { const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 1f1093a179538c..8d2ebb575c35e0 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -31,6 +31,7 @@ export interface EncryptedSavedObjectTypeRegistration { readonly type: string; readonly attributesToEncrypt: ReadonlySet<string | AttributeToEncrypt>; readonly attributesToExcludeFromAAD?: ReadonlySet<string>; + readonly allowPredefinedID?: boolean; } /** @@ -144,6 +145,25 @@ export class EncryptedSavedObjectsService { return this.typeDefinitions.has(type); } + /** + * Checks whether ID can be specified for the provided saved object. + * + * If the type isn't registered as an encrypted saved object, or when overwriting an existing + * saved object with a version specified, this will return "true". + * + * @param type Saved object type. + * @param version Saved object version number which changes on each successful write operation. + * Can be used in conjunction with `overwrite` for implementing optimistic concurrency + * control. + * @param overwrite Overwrite existing documents. + */ + public canSpecifyID(type: string, version?: string, overwrite?: boolean) { + const typeDefinition = this.typeDefinitions.get(type); + return ( + typeDefinition === undefined || typeDefinition.allowPredefinedID || !!(version && overwrite) + ); + } + /** * Takes saved object attributes for the specified type and, depending on the type definition, * either decrypts or strips encrypted attributes (e.g. in case AAD or encryption key has changed diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 6346e73e6ba51f..3c722ccfabae24 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -30,6 +30,11 @@ beforeEach(() => { { key: 'attrNotSoSecret', dangerouslyExposeValue: true }, ]), }, + { + type: 'known-type-predefined-id', + attributesToEncrypt: new Set(['attrSecret']), + allowPredefinedID: true, + }, ]); wrapper = new EncryptedSavedObjectsClientWrapper({ @@ -72,16 +77,36 @@ describe('#create', () => { expect(mockBaseClient.create).toHaveBeenCalledWith('unknown-type', attributes, options); }); - it('fails if type is registered and ID is specified', async () => { + it('fails if type is registered without allowPredefinedID and ID is specified', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; await expect(wrapper.create('known-type', attributes, { id: 'some-id' })).rejects.toThrowError( - 'Predefined IDs are not allowed for saved objects with encrypted attributes.' + 'Predefined IDs are not allowed for encrypted saved objects of type "known-type".' ); expect(mockBaseClient.create).not.toHaveBeenCalled(); }); + it('succeeds if type is registered with allowPredefinedID and ID is specified', async () => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const mockedResponse = { + id: 'some-id', + type: 'known-type-predefined-id', + attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + references: [], + }; + + mockBaseClient.create.mockResolvedValue(mockedResponse); + await expect( + wrapper.create('known-type-predefined-id', attributes, { id: 'some-id' }) + ).resolves.toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrThree: 'three' }, + }); + + expect(mockBaseClient.create).toHaveBeenCalled(); + }); + it('allows a specified ID when overwriting an existing object', async () => { const attributes = { attrOne: 'one', @@ -299,7 +324,7 @@ describe('#bulkCreate', () => { ); }); - it('fails if ID is specified for registered type', async () => { + it('fails if ID is specified for registered type without allowPredefinedID', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; const bulkCreateParams = [ @@ -308,12 +333,48 @@ describe('#bulkCreate', () => { ]; await expect(wrapper.bulkCreate(bulkCreateParams)).rejects.toThrowError( - 'Predefined IDs are not allowed for saved objects with encrypted attributes.' + 'Predefined IDs are not allowed for encrypted saved objects of type "known-type".' ); expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled(); }); + it('succeeds if ID is specified for registered type with allowPredefinedID', async () => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { namespace: 'some-namespace' }; + const mockedResponse = { + saved_objects: [ + { + id: 'some-id', + type: 'known-type-predefined-id', + attributes, + references: [], + }, + { + id: 'some-id', + type: 'unknown-type', + attributes, + references: [], + }, + ], + }; + mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + + const bulkCreateParams = [ + { id: 'some-id', type: 'known-type-predefined-id', attributes }, + { type: 'unknown-type', attributes }, + ]; + + await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({ + saved_objects: [ + { ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } }, + mockedResponse.saved_objects[1], + ], + }); + + expect(mockBaseClient.bulkCreate).toHaveBeenCalled(); + }); + it('allows a specified ID when overwriting an existing object', async () => { const attributes = { attrOne: 'one', diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 59309ab67e772e..ddef9f477433ca 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -68,16 +68,14 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon } // Saved objects with encrypted attributes should have IDs that are hard to guess especially - // since IDs are part of the AAD used during encryption, that's why we control them within this - // wrapper and don't allow consumers to specify their own IDs directly. - - // only allow a specified ID if we're overwriting an existing ESO with a Version - // this helps us ensure that the document really was previously created using ESO - // and not being used to get around the specified ID limitation - const canSpecifyID = options.overwrite && options.version; - if (options.id && !canSpecifyID) { + // since IDs are part of the AAD used during encryption. Types can opt-out of this restriction, + // when necessary, but it's much safer for this wrapper to generate them. + if ( + options.id && + !this.options.service.canSpecifyID(type, options.version, options.overwrite) + ) { throw new Error( - 'Predefined IDs are not allowed for saved objects with encrypted attributes.' + `Predefined IDs are not allowed for encrypted saved objects of type "${type}".` ); } @@ -118,10 +116,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // Saved objects with encrypted attributes should have IDs that are hard to guess especially // since IDs are part of the AAD used during encryption, that's why we control them within this // wrapper and don't allow consumers to specify their own IDs directly unless overwriting the original document. - const canSpecifyID = options?.overwrite && object.version; - if (object.id && !canSpecifyID) { + if ( + object.id && + !this.options.service.canSpecifyID(object.type, object.version, options?.overwrite) + ) { throw new Error( - 'Predefined IDs are not allowed for saved objects with encrypted attributes.' + `Predefined IDs are not allowed for encrypted saved objects of type "${object.type}".` ); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/constants.ts new file mode 100644 index 00000000000000..54b0d47ee02d8e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const DOCUMENTS_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.documents.title', { + defaultMessage: 'Documents', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx new file mode 100644 index 00000000000000..a289ec78d9e801 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; +import '../../../__mocks__/react_router_history.mock'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useParams } from 'react-router-dom'; +import { EuiPageContent, EuiBasicTable } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { DocumentDetail } from '.'; +import { ResultFieldValue } from '../result_field_value'; + +describe('DocumentDetail', () => { + const values = { + dataLoading: false, + fields: [], + }; + + const actions = { + deleteDocument: jest.fn(), + getDocumentDetails: jest.fn(), + setFields: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + + (useParams as jest.Mock).mockImplementationOnce(() => ({ + documentId: '1', + })); + }); + + it('renders', () => { + const wrapper = shallow(<DocumentDetail engineBreadcrumb={['test']} />); + expect(wrapper.find(EuiPageContent).length).toBe(1); + }); + + it('initializes data on mount', () => { + shallow(<DocumentDetail engineBreadcrumb={['test']} />); + expect(actions.getDocumentDetails).toHaveBeenCalledWith('1'); + }); + + it('calls setFields on unmount', () => { + shallow(<DocumentDetail engineBreadcrumb={['test']} />); + unmountHandler(); + expect(actions.setFields).toHaveBeenCalledWith([]); + }); + + it('will show a loader while data is loading', () => { + setMockValues({ + ...values, + dataLoading: true, + }); + + const wrapper = shallow(<DocumentDetail engineBreadcrumb={['test']} />); + + expect(wrapper.find(Loading).length).toBe(1); + }); + + describe('field values list', () => { + let columns: any; + + const field = { + name: 'Foo', + value: 'Bar', + type: 'string', + }; + + beforeEach(() => { + const wrapper = shallow(<DocumentDetail engineBreadcrumb={['test']} />); + columns = wrapper.find(EuiBasicTable).props().columns; + }); + + it('will render the field name in the first column', () => { + const column = columns[0]; + const wrapper = shallow(<div>{column.render(field)}</div>); + expect(wrapper.text()).toEqual('Foo'); + }); + + it('will render the field value in the second column', () => { + const column = columns[1]; + const wrapper = shallow(<div>{column.render(field)}</div>); + expect(wrapper.find(ResultFieldValue).props()).toEqual({ + raw: 'Bar', + type: 'string', + }); + }); + }); + + it('will delete the document when the delete button is pressed', () => { + const wrapper = shallow(<DocumentDetail engineBreadcrumb={['test']} />); + const button = wrapper.find('[data-test-subj="DeleteDocumentButton"]'); + + button.simulate('click'); + + expect(actions.deleteDocument).toHaveBeenCalledWith('1'); + }); + + it('correctly decodes document IDs', () => { + (useParams as jest.Mock).mockReturnValueOnce({ documentId: 'hello%20world%20%26%3F!' }); + const wrapper = shallow(<DocumentDetail engineBreadcrumb={['test']} />); + expect(wrapper.find('h1').text()).toEqual('Document: hello world &?!'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx new file mode 100644 index 00000000000000..017f5a2f67ad07 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.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, { useEffect } from 'react'; +import { useActions, useValues } from 'kea'; +import { useParams } from 'react-router-dom'; + +import { + EuiButton, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiPageContentBody, + EuiPageContent, + EuiBasicTable, + EuiBasicTableColumn, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Loading } from '../../../shared/loading'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { FlashMessages } from '../../../shared/flash_messages'; +import { ResultFieldValue } from '../result_field_value'; + +import { DocumentDetailLogic } from './document_detail_logic'; +import { FieldDetails } from './types'; +import { DOCUMENTS_TITLE } from './constants'; + +const DOCUMENT_DETAIL_TITLE = (documentId: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.title', { + defaultMessage: 'Document: {documentId}', + values: { documentId }, + }); +interface Props { + engineBreadcrumb: string[]; +} + +export const DocumentDetail: React.FC<Props> = ({ engineBreadcrumb }) => { + const { dataLoading, fields } = useValues(DocumentDetailLogic); + const { deleteDocument, getDocumentDetails, setFields } = useActions(DocumentDetailLogic); + + const { documentId } = useParams() as { documentId: string }; + + useEffect(() => { + getDocumentDetails(documentId); + return () => { + setFields([]); + }; + }, []); + + if (dataLoading) { + return <Loading />; + } + + const columns: Array<EuiBasicTableColumn<FieldDetails>> = [ + { + name: i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.fieldHeader', { + defaultMessage: 'Field', + }), + width: '20%', + render: (field: FieldDetails) => field.name, + }, + { + name: i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.valueHeader', { + defaultMessage: 'Value', + }), + width: '80%', + render: ({ value, type }: FieldDetails) => <ResultFieldValue raw={value} type={type} />, + }, + ]; + + return ( + <> + <SetPageChrome + trail={[...engineBreadcrumb, DOCUMENTS_TITLE, decodeURIComponent(documentId)]} + /> + <EuiPageHeader> + <EuiPageHeaderSection> + <EuiTitle size="l"> + <h1>{DOCUMENT_DETAIL_TITLE(decodeURIComponent(documentId))}</h1> + </EuiTitle> + </EuiPageHeaderSection> + <EuiPageHeaderSection> + <EuiButton + color="danger" + iconType="trash" + onClick={() => deleteDocument(documentId)} + data-test-subj="DeleteDocumentButton" + > + {i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.deleteButton', { + defaultMessage: 'Delete', + })} + </EuiButton> + </EuiPageHeaderSection> + </EuiPageHeader> + <EuiPageContent> + <EuiPageContentBody> + <FlashMessages /> + <EuiBasicTable columns={columns} items={fields} /> + </EuiPageContentBody> + </EuiPageContent> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index 782b8159c94a10..4afa3f7aee5c88 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -16,6 +16,11 @@ jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'engine1' } }, })); +jest.mock('../../../shared/kibana', () => ({ + KibanaLogic: { values: { navigateToUrl: jest.fn() } }, +})); +import { KibanaLogic } from '../../../shared/kibana'; + jest.mock('../../../shared/flash_messages', () => ({ setQueuedSuccessMessage: jest.fn(), flashAPIErrors: jest.fn(), @@ -98,7 +103,8 @@ describe('DocumentDetailLogic', () => { } catch { // Do nothing } - expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); + expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred', { isQueued: true }); + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith('/engines/engine1/documents'); }); }); @@ -117,7 +123,7 @@ describe('DocumentDetailLogic', () => { confirmSpy.mockRestore(); }); - it('will call an API endpoint and show a success message', async () => { + it('will call an API endpoint and show a success message on the documents page', async () => { mount(); DocumentDetailLogic.actions.deleteDocument('1'); @@ -126,6 +132,7 @@ describe('DocumentDetailLogic', () => { expect(setQueuedSuccessMessage).toHaveBeenCalledWith( 'Successfully marked document for deletion. It will be deleted momentarily.' ); + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith('/engines/engine1/documents'); }); it('will do nothing if not confirmed', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts index 87bf149fb1680b..62db2bf1723543 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -11,6 +11,8 @@ import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; import { FieldDetails } from './types'; +import { KibanaLogic } from '../../../shared/kibana'; +import { ENGINE_DOCUMENTS_PATH, getEngineRoute } from '../../routes'; interface DocumentDetailLogicValues { dataLoading: boolean; @@ -65,13 +67,17 @@ export const DocumentDetailLogic = kea<DocumentDetailLogicType>({ try { const { http } = HttpLogic.values; - // TODO: Handle 404s const response = await http.get( `/api/app_search/engines/${engineName}/documents/${documentId}` ); actions.setFields(response.fields); } catch (e) { - flashAPIErrors(e); + // If an error occurs trying to load this document, it will typically be a 404, or some other + // error that will prevent the page from loading, so redirect to the documents page and + // show the error + flashAPIErrors(e, { isQueued: true }); + const engineRoute = getEngineRoute(engineName); + KibanaLogic.values.navigateToUrl(engineRoute + ENGINE_DOCUMENTS_PATH); } }, deleteDocument: async ({ documentId }) => { @@ -82,7 +88,8 @@ export const DocumentDetailLogic = kea<DocumentDetailLogicType>({ const { http } = HttpLogic.values; await http.delete(`/api/app_search/engines/${engineName}/documents/${documentId}`); setQueuedSuccessMessage(DELETE_SUCCESS); - // TODO Handle routing after success + const engineRoute = getEngineRoute(engineName); + KibanaLogic.values.navigateToUrl(engineRoute + ENGINE_DOCUMENTS_PATH); } catch (e) { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx new file mode 100644 index 00000000000000..023ae06767abe2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiPageContent, + EuiPageContentBody, +} from '@elastic/eui'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { FlashMessages } from '../../../shared/flash_messages'; +import { DOCUMENTS_TITLE } from './constants'; + +interface Props { + engineBreadcrumb: string[]; +} + +export const Documents: React.FC<Props> = ({ engineBreadcrumb }) => { + return ( + <> + <SetPageChrome trail={[...engineBreadcrumb, DOCUMENTS_TITLE]} /> + <EuiPageHeader> + <EuiPageHeaderSection> + <EuiTitle size="l"> + <h1>{DOCUMENTS_TITLE}</h1> + </EuiTitle> + </EuiPageHeaderSection> + </EuiPageHeader> + <EuiPageContent> + <EuiPageContentBody> + <FlashMessages /> + </EuiPageContentBody> + </EuiPageContent> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts index d374098d707885..b67e444939c0e5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts @@ -6,3 +6,5 @@ export { DocumentDetailLogic } from './document_detail_logic'; export { DocumentsLogic } from './documents_logic'; +export { Documents } from './documents'; +export { DocumentDetail } from './document_detail'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index a7ac6f203b1f72..35389bbe4b3ba7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -111,8 +111,8 @@ export const EngineNav: React.FC = () => { )} {canViewEngineDocuments && ( <SideNavLink - isExternal - to={getAppSearchUrl(engineRoute + ENGINE_DOCUMENTS_PATH)} + to={engineRoute + ENGINE_DOCUMENTS_PATH} + shouldShowActiveForSubroutes={true} data-test-subj="EngineDocumentsLink" > {DOCUMENTS_TITLE} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index f586106924f2cb..9e0b043a87364a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -19,7 +19,8 @@ import { ENGINES_PATH, ENGINE_PATH, ENGINE_ANALYTICS_PATH, - // ENGINE_DOCUMENTS_PATH, + ENGINE_DOCUMENTS_PATH, + ENGINE_DOCUMENT_DETAIL_PATH, // ENGINE_SCHEMA_PATH, // ENGINE_CRAWLER_PATH, // META_ENGINE_SOURCE_ENGINES_PATH, @@ -49,6 +50,7 @@ import { Loading } from '../../../shared/loading'; import { EngineOverview } from '../engine_overview'; import { EngineLogic } from './'; +import { DocumentDetail, Documents } from '../documents'; export const EngineRouter: React.FC = () => { const { @@ -99,6 +101,12 @@ export const EngineRouter: React.FC = () => { <div data-test-subj="AnalyticsTODO">Just testing right now</div> </Route> )} + <Route path={ENGINE_PATH + ENGINE_DOCUMENT_DETAIL_PATH}> + <DocumentDetail engineBreadcrumb={engineBreadcrumb} /> + </Route> + <Route path={ENGINE_PATH + ENGINE_DOCUMENTS_PATH}> + <Documents engineBreadcrumb={engineBreadcrumb} /> + </Route> <Route> <SetPageChrome trail={[...engineBreadcrumb, OVERVIEW_TITLE]} /> <EngineOverview /> diff --git a/x-pack/plugins/apm/scripts/shared/stamp-logger.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/index.ts similarity index 64% rename from x-pack/plugins/apm/scripts/shared/stamp-logger.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/index.ts index 65d24bbae7008b..43755eaadaf812 100644 --- a/x-pack/plugins/apm/scripts/shared/stamp-logger.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/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 { ResultFieldValue } from './result_field_value'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.scss new file mode 100644 index 00000000000000..13a771d24adc98 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.scss @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.enterpriseSearchDataType { + &--number, + &--float { + color: $euiColorAccentText; + font-family: $euiCodeFontFamily; + } + + &--location { + color: $euiColorSuccessText; + font-family: $euiCodeFontFamily; + } + + &--date { + font-family: $euiCodeFontFamily; + } +} + +.enterpriseSearchResultHighlight { + color: $euiColorPrimary; + font-weight: $euiFontWeightSemiBold; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.test.tsx new file mode 100644 index 00000000000000..227700bdd0cd20 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.test.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { ResultFieldValue } from '.'; + +describe('ResultFieldValue', () => { + describe('when no raw or snippet values are provided', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue type="string" />); + }); + + it('will render a dash', () => { + expect(wrapper.text()).toEqual('—'); + }); + }); + + describe('when there is only a raw value', () => { + describe('and the value is a string', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue raw="foo" type="string" />); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('foo'); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--string'); + }); + }); + + describe('and the value is a string array', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue raw={['foo', 'bar']} type="string" />); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('["foo", "bar"]'); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--string'); + }); + }); + + describe('and the value is a number', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue raw={1} type="number" />); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('1'); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--number'); + }); + }); + + describe('and the value is an array of numbers', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue raw={[1, 2]} type="number" />); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('[1, 2]'); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--number'); + }); + }); + + describe('and the value is a location', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue raw={'44.6, -110.5'} type="location" />); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('44.6, -110.5'); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--location'); + }); + }); + + describe('and the value is an array of locations', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow( + <ResultFieldValue raw={['44.6, -110.5', '44.7, -111.0']} type="location" /> + ); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('[44.6, -110.5, 44.7, -111.0]'); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--location'); + }); + }); + + describe('and the value is a date', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow(<ResultFieldValue raw="1872-03-01T06:00:00Z" type="date" />); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('1872-03-01T06:00:00Z'); + }); + + it('will have the appropriate type class on outer div', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--date'); + }); + }); + + describe('and the value is an array of dates', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow( + <ResultFieldValue raw={['1872-03-01T06:00:00Z', '1472-04-01T06:00:00Z']} type="date" /> + ); + }); + + it('will render a display value', () => { + expect(wrapper.text()).toEqual('[1872-03-01T06:00:00Z, 1472-04-01T06:00:00Z]'); + }); + + it('will have the appropriate type class on outer div', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--date'); + }); + }); + }); + + describe('when there is a snippet value', () => { + let wrapper: ShallowWrapper; + beforeAll(() => { + wrapper = shallow( + <ResultFieldValue + raw="I am a long description of a thing" + snippet="a <em>long</em> description" + type="string" + /> + ); + }); + + it('will render content as html with class names appended to em tags', () => { + expect(wrapper.find('div').html()).toContain( + 'a <em class="enterpriseSearchResultHighlight">long</em> description' + ); + }); + + it('will have the appropriate type class', () => { + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--string'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.tsx new file mode 100644 index 00000000000000..9ee0f1e0ba043b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.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 React from 'react'; + +import classNames from 'classnames'; + +import { Raw, Snippet } from '../../types'; + +import './result_field_value.scss'; + +const isNotNumeric = (raw: string | number): boolean => { + if (typeof raw === 'number') return false; + return isNaN(parseFloat(raw)); +}; + +const getRawArrayDisplay = (rawArray: Array<string | number>): string => { + return `[${rawArray.map((raw) => (isNotNumeric(raw) ? `"${raw}"` : raw)).join(', ')}]`; +}; + +const parseHighlights = (highlight: string): string => { + return highlight.replace( + /<em>(.+?)<\/em>/gi, + '<em class="enterpriseSearchResultHighlight">$1</em>' + ); +}; + +const isFieldValueEmpty = (type?: string, raw?: Raw, snippet?: Snippet) => { + const isNumber = type === 'number'; + if (isNumber) { + return raw === null; + } + + return !snippet && !raw; +}; + +interface Props { + raw?: Raw; + snippet?: Snippet; + type?: string; + className?: string; +} + +export const ResultFieldValue: React.FC<Props> = ({ snippet, raw, type, className }) => { + const isEmpty = isFieldValueEmpty(type, raw, snippet); + if (isEmpty) return <>—</>; + const classes = classNames({ [`enterpriseSearchDataType--${type}`]: !!type }, className); + return ( + <div + className={classes} + /* + * Justification for dangerouslySetInnerHTML: + * The App Search server will return html highlights within fields. This data is sanitized by + * the App Search server is considered safe for use. + */ + dangerouslySetInnerHTML={snippet ? { __html: parseHighlights(snippet) } : undefined} // eslint-disable-line react/no-danger + > + {!!snippet ? null : Array.isArray(raw) ? getRawArrayDisplay(raw) : raw} + </div> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index 7c22a45757a937..9af1ff0293faef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -7,3 +7,5 @@ export * from '../../../common/types/app_search'; export { Role, RoleTypes, AbilityTypes } from './utils/role'; export { Engine } from './components/engine/types'; +export type Raw = string | string[] | number | number[]; +export type Snippet = string; 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 bd57958b0cb88a..c1f60f2d630490 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 6e787c905d4001..57a6b1d3bb932a 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 fa9f9c36052a10..d1dcf621150a6d 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 aac7c684218aa8..49a57fcb2b00d2 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 8c967e68299b55..d7f67620e7968d 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 ea699af45ccd2f..28b4f5325dcb78 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 658d90d8096525..db24379bb46ba7 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 b30d83f24f261a..00000000000000 --- 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 2c5ebcd38f5a8a..00000000000000 --- 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 58879649b83cb3..706f3e79cc2790 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 00000000000000..e32bda9089701d --- /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 f69850f166aee2..d85de565b4d8e4 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/kibana.json b/x-pack/plugins/fleet/kibana.json index 81b56682b47e16..2fcbef75b9832b 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 a22e4e14055e3a..9ebc8ea9380a9f 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 80ecaa24932785..639a3e41b39fa8 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 9963753651671a..ecd4227a54b655 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 29843f6a3e5b1d..6026a5579f65b6 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 ed38e1a5ce4a13..40654645ecd3f2 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 d8535183bb84e9..da5be82049c8e5 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 dad2eaa1d8e0f3..f425831f6d6bca 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 bf8f33297882e4..00000000000000 --- 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 58537b2075c160..5faa3bfcab4af8 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 1b17c5cb0b1f36..40c0689905932b 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 51c897b3661ccb..61a5f1eabc2afe 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 376de7e2e6a075..93bfe489a1bf4a 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 03efe20f96a51c..e49ef152f8306d 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 4b21a15a736455..bca0e2889483f6 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 08f6244242a3d0..93ad9977800156 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 41201f9612f13e..9e2a7ae8f8f47b 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 41704f69958a01..7afb028dded2a2 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 773d53484147aa..7b0075e160c47f 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 8de40edc403311..e86ac9e3bd03c4 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 a837ed33e41106..62792b84105abb 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 fe3955c84dec3e..b33976d53fe95e 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 7528c923f0abde..0099fb3c84d128 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 bfc10848d378f9..c0db51873e52ef 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 f10f36174fe822..364df44a59e186 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 c1a1b3862728db..00000000000000 --- 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 00000000000000..610c2feacf99e1 --- /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 00000000000000..39d383cad503d2 --- /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 00000000000000..b56e27356ef342 --- /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 00000000000000..bc3cfd84d2379a --- /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 00000000000000..b034168dc8a15e --- /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 00000000000000..e033781a850a02 --- /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 00000000000000..ae2385d7142192 --- /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 b512ca230080d3..00000000000000 --- 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 8e6ddd09593582..128f803bb2f2e1 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 f808f4ade107b5..00000000000000 --- 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 fd8de709c172a6..00000000000000 --- 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 dbe18ab3337368..00000000000000 --- 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 7d60ae23deac6f..f3714bbb532236 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 758497607c057e..b90758335dc75a 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 656493e31e5f56..840e47c5cd1f78 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 a2daf2d10c2715..da2bb8adf1b35d 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 46e291e73fa786..90726b54d283ac 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 1b3935a86f65c4..180ad5e4953b82 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 43ad7208c3d810..6b7fca9e086aa2 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 78e8be4679dc3c..ed607e361bd6e5 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 7e5d07b2319d30..71cd417a256c37 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 60ee791ace5ebc..8fee44018f0a05 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 533c2736811220..c614518c1930bb 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 7004a602627c1c..8ced0734a39679 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 a453a7f2e28cb8..3d2babae8eb2ea 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 00000000000000..e8366d99b63916 --- /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 2535a53589bd97..0e72693db9e2d0 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 e9704cd16b2192..b5fef901d123d0 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 58f84e86713853..10f538b3112c65 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/applications/fleet/types/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts index fbede8af95b664..a8a961ca949b63 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts @@ -19,9 +19,7 @@ export interface UIExtensionsStorage { * UI Component Extension is used on the pages displaying the ability to edit an * Integration Policy */ -export type PackagePolicyEditExtensionComponent = ComponentType< - PackagePolicyEditExtensionComponentProps ->; +export type PackagePolicyEditExtensionComponent = ComponentType<PackagePolicyEditExtensionComponentProps>; export interface PackagePolicyEditExtensionComponentProps { /** The current integration policy being edited */ @@ -51,9 +49,7 @@ export interface PackagePolicyEditExtension { * UI Component Extension is used on the pages displaying the ability to Create an * Integration Policy */ -export type PackagePolicyCreateExtensionComponent = ComponentType< - PackagePolicyCreateExtensionComponentProps ->; +export type PackagePolicyCreateExtensionComponent = ComponentType<PackagePolicyCreateExtensionComponentProps>; export interface PackagePolicyCreateExtensionComponentProps { /** The integration policy being created */ diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 7e523b3fa594a8..31b53f41b3a913 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 3d30acd3f8e01e..1fe7013944fd71 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 c8aef287e4432d..91098c87c312a8 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 e4ed386802c3af..90fb34efd4817e 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/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 5e075cbbcdf5ee..eff7d3c3c5cf32 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -35,9 +35,9 @@ import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; -export const getAgentHandler: RequestHandler<TypeOf< - typeof GetOneAgentRequestSchema.params ->> = async (context, request, response) => { +export const getAgentHandler: RequestHandler< + TypeOf<typeof GetOneAgentRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { const agent = await AgentService.getAgent(soClient, request.params.agentId); @@ -94,9 +94,9 @@ export const getAgentEventsHandler: RequestHandler< } }; -export const deleteAgentHandler: RequestHandler<TypeOf< - typeof DeleteAgentRequestSchema.params ->> = async (context, request, response) => { +export const deleteAgentHandler: RequestHandler< + TypeOf<typeof DeleteAgentRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { await AgentService.deleteAgent(soClient, request.params.agentId); diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 91a033a5379dfb..25aaf5f9a46562 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -70,9 +70,9 @@ export const getAgentPoliciesHandler: RequestHandler< } }; -export const getOneAgentPolicyHandler: RequestHandler<TypeOf< - typeof GetOneAgentPolicyRequestSchema.params ->> = async (context, request, response) => { +export const getOneAgentPolicyHandler: RequestHandler< + TypeOf<typeof GetOneAgentPolicyRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { const agentPolicy = await agentPolicyService.get(soClient, request.params.agentPolicyId); diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index 50014b388e0fdf..afecd7bd7d828b 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -60,9 +60,9 @@ export const postEnrollmentApiKeyHandler: RequestHandler< } }; -export const deleteEnrollmentApiKeyHandler: RequestHandler<TypeOf< - typeof DeleteEnrollmentAPIKeyRequestSchema.params ->> = async (context, request, response) => { +export const deleteEnrollmentApiKeyHandler: RequestHandler< + TypeOf<typeof DeleteEnrollmentAPIKeyRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { await APIKeyService.deleteEnrollmentApiKey(soClient, request.params.keyId); @@ -80,9 +80,9 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler<TypeOf< } }; -export const getOneEnrollmentApiKeyHandler: RequestHandler<TypeOf< - typeof GetOneEnrollmentAPIKeyRequestSchema.params ->> = async (context, request, response) => { +export const getOneEnrollmentApiKeyHandler: RequestHandler< + TypeOf<typeof GetOneEnrollmentAPIKeyRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { const apiKey = await APIKeyService.getEnrollmentAPIKey(soClient, request.params.keyId); diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index ce03d0eeb38269..aa6160bbd8914e 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -246,9 +246,9 @@ export const installPackageByUploadHandler: RequestHandler< } }; -export const deletePackageHandler: RequestHandler<TypeOf< - typeof DeletePackageRequestSchema.params ->> = async (context, request, response) => { +export const deletePackageHandler: RequestHandler< + TypeOf<typeof DeletePackageRequestSchema.params> +> = async (context, request, response) => { try { const { pkgkey } = request.params; const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/fleet/server/routes/output/handler.ts b/x-pack/plugins/fleet/server/routes/output/handler.ts index 9cdab757acc879..1c46915cac3fcc 100644 --- a/x-pack/plugins/fleet/server/routes/output/handler.ts +++ b/x-pack/plugins/fleet/server/routes/output/handler.ts @@ -29,9 +29,9 @@ export const getOutputsHandler: RequestHandler = async (context, request, respon } }; -export const getOneOuputHandler: RequestHandler<TypeOf< - typeof GetOneOutputRequestSchema.params ->> = async (context, request, response) => { +export const getOneOuputHandler: RequestHandler< + TypeOf<typeof GetOneOutputRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { const output = await outputService.get(soClient, request.params.outputId); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 3a2b9ba7a744f7..b154aa2a2782fd 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -41,9 +41,9 @@ export const getPackagePoliciesHandler: RequestHandler< } }; -export const getOnePackagePolicyHandler: RequestHandler<TypeOf< - typeof GetOnePackagePolicyRequestSchema.params ->> = async (context, request, response) => { +export const getOnePackagePolicyHandler: RequestHandler< + TypeOf<typeof GetOnePackagePolicyRequestSchema.params> +> = async (context, request, response) => { const soClient = context.core.savedObjects.client; const { packagePolicyId } = request.params; const notFoundResponse = () => 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 04aa1767b4f14a..6032159fdfcc52 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { ArchiveEntry } from './index'; -import { InstallSource, ArchivePackage, RegistryPackage } from '../../../../common'; +import { ArchivePackage, RegistryPackage } from '../../../../common'; const archiveEntryCache: Map<ArchiveEntry['path'], ArchiveEntry['buffer']> = new Map(); export const getArchiveEntry = (key: string) => archiveEntryCache.get(key); @@ -16,7 +16,6 @@ export const deleteArchiveEntry = (key: string) => archiveEntryCache.delete(key) export interface SharedKey { name: string; version: string; - installSource: InstallSource; } type SharedKeyString = string; @@ -31,18 +30,10 @@ export const deleteArchiveFilelist = (keyArgs: SharedKey) => archiveFilelistCache.delete(sharedKey(keyArgs)); const packageInfoCache: Map<SharedKeyString, ArchivePackage | RegistryPackage> = new Map(); -const sharedKey = ({ name, version, installSource }: SharedKey) => - `${name}-${version}-${installSource}`; +const sharedKey = ({ name, version }: SharedKey) => `${name}-${version}`; export const getPackageInfo = (args: SharedKey) => { - const packageInfo = packageInfoCache.get(sharedKey(args)); - if (args.installSource === 'registry') { - return packageInfo as RegistryPackage; - } else if (args.installSource === 'upload') { - return packageInfo as ArchivePackage; - } else { - throw new Error(`Unknown installSource: ${args.installSource}`); - } + return packageInfoCache.get(sharedKey(args)); }; export const getArchivePackage = (args: SharedKey) => { @@ -57,10 +48,9 @@ export const getArchivePackage = (args: SharedKey) => { export const setPackageInfo = ({ name, version, - installSource, packageInfo, }: SharedKey & { packageInfo: ArchivePackage | RegistryPackage }) => { - const key = sharedKey({ name, version, installSource }); + const key = sharedKey({ name, version }); return packageInfoCache.set(key, packageInfo); }; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index ddaf9b640c86a1..81d4fa6869e3ba 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -49,7 +49,7 @@ export async function unpackBufferToCache({ paths.push(path); } }); - setArchiveFilelist({ name, version, installSource }, paths); + setArchiveFilelist({ name, version }, paths); return paths; } @@ -85,18 +85,18 @@ export async function unpackBufferEntries( return entries; } -export const deletePackageCache = ({ name, version, installSource }: SharedKey) => { +export const deletePackageCache = ({ name, version }: SharedKey) => { // get cached archive filelist - const paths = getArchiveFilelist({ name, version, installSource }); + const paths = getArchiveFilelist({ name, version }); // delete cached archive filelist - deleteArchiveFilelist({ name, version, installSource }); + deleteArchiveFilelist({ name, version }); // delete cached archive files // this has been populated in unpackBufferToCache() paths?.forEach(deleteArchiveEntry); - deletePackageInfo({ name, version, installSource }); + deletePackageInfo({ name, version }); }; export function getPathParts(path: string): AssetParts { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 7ca2a32cf770c8..9d0d25ba55fb8c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -298,9 +298,9 @@ describe('test transform install', () => { (getInstallationObject as jest.MockedFunction< typeof getInstallationObject >).mockReturnValueOnce( - Promise.resolve(({ attributes: { installed_es: [] } } as unknown) as SavedObject< - Installation - >) + Promise.resolve(({ + attributes: { installed_es: [] }, + } as unknown) as SavedObject<Installation>) ); legacyScopedClusterClient.callAsCurrentUser = jest.fn(); await installTransform( diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts index ee5257b8a3ef62..bdb6744745c97a 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts @@ -9,13 +9,12 @@ import { INDEX_PATTERN_SAVED_OBJECT_TYPE, INDEX_PATTERN_PLACEHOLDER_SUFFIX, } from '../../../../constants'; -import * as Registry from '../../registry'; import { loadFieldsFromYaml, Fields, Field } from '../../fields/field'; -import { getPackageKeysByStatus } from '../../packages/get'; import { dataTypes, installationStatuses } from '../../../../../common/constants'; -import { ValueOf } from '../../../../../common/types'; +import { ArchivePackage, InstallSource, ValueOf } from '../../../../../common/types'; import { RegistryPackage, CallESAsCurrentUser, DataType } from '../../../../types'; import { appContextService } from '../../../../services'; +import { getPackageFromSource, getPackageSavedObjects } from '../../packages/get'; interface FieldFormatMap { [key: string]: FieldFormatMapItem; @@ -73,45 +72,54 @@ export interface IndexPatternField { } export const indexPatternTypes = Object.values(dataTypes); -// TODO: use a function overload and make pkgName and pkgVersion required for install/update -// and not for an update removal. or separate out the functions export async function installIndexPatterns( savedObjectsClient: SavedObjectsClientContract, pkgName?: string, - pkgVersion?: string + pkgVersion?: string, + installSource?: InstallSource ) { // get all user installed packages - const installedPackages = await getPackageKeysByStatus( - savedObjectsClient, - installationStatuses.Installed + const installedPackagesRes = await getPackageSavedObjects(savedObjectsClient); + const installedPackagesSavedObjects = installedPackagesRes.saved_objects.filter( + (so) => so.attributes.install_status === installationStatuses.Installed ); - const packageVersionsToFetch = [...installedPackages]; - if (pkgName && pkgVersion) { - const packageToInstall = packageVersionsToFetch.find((pkg) => pkg.pkgName === pkgName); + const packagesToFetch = installedPackagesSavedObjects.reduce< + Array<{ name: string; version: string; installSource: InstallSource }> + >((acc, pkgSO) => { + acc.push({ + name: pkgSO.attributes.name, + version: pkgSO.attributes.version, + installSource: pkgSO.attributes.install_source, + }); + return acc; + }, []); + if (pkgName && pkgVersion && installSource) { + const packageToInstall = packagesToFetch.find((pkgSO) => pkgSO.name === pkgName); if (packageToInstall) { // set the version to the one we want to install - // if we're installing for the first time the number will be the same + // if we're reinstalling the number will be the same // if this is an upgrade then we'll be modifying the version number to the upgrade version - packageToInstall.pkgVersion = pkgVersion; + packageToInstall.version = pkgVersion; } else { - // this will likely not happen because the saved objects should already have the package we're trying - // install which means that it should have been found in the case above - packageVersionsToFetch.push({ pkgName, pkgVersion }); + // if we're installing for the first time, add to the list + packagesToFetch.push({ name: pkgName, version: pkgVersion, installSource }); } } // get each package's registry info - const packageVersionsFetchInfoPromise = packageVersionsToFetch.map((pkg) => - Registry.fetchInfo(pkg.pkgName, pkg.pkgVersion) + const packagesToFetchPromise = packagesToFetch.map((pkg) => + getPackageFromSource({ + pkgName: pkg.name, + pkgVersion: pkg.version, + pkgInstallSource: pkg.installSource, + }) ); - - const packageVersionsInfo = await Promise.all(packageVersionsFetchInfoPromise); - + const packages = await Promise.all(packagesToFetchPromise); // for each index pattern type, create an index pattern indexPatternTypes.forEach(async (indexPatternType) => { // if this is an update because a package is being uninstalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern - if (!pkgName && installedPackages.length === 0) { + if (!pkgName && installedPackagesSavedObjects.length === 0) { try { await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); } catch (err) { @@ -119,9 +127,9 @@ export async function installIndexPatterns( } return; } - + const packagesWithInfo = packages.map((pkg) => pkg.packageInfo); // get all data stream fields from all installed packages - const fields = await getAllDataStreamFieldsByType(packageVersionsInfo, indexPatternType); + const fields = await getAllDataStreamFieldsByType(packagesWithInfo, indexPatternType); const kibanaIndexPattern = createIndexPattern(indexPatternType, fields); // create or overwrite the index pattern await savedObjectsClient.create(INDEX_PATTERN_SAVED_OBJECT_TYPE, kibanaIndexPattern, { @@ -134,7 +142,7 @@ export async function installIndexPatterns( // loops through all given packages and returns an array // of all fields from all data streams matching data stream type export const getAllDataStreamFieldsByType = async ( - packages: RegistryPackage[], + packages: Array<RegistryPackage | ArchivePackage>, dataStreamType: ValueOf<DataType> ): Promise<Fields> => { const dataStreamsPromises = packages.reduce<Array<Promise<Field[]>>>((acc, pkg) => { @@ -143,9 +151,9 @@ export const getAllDataStreamFieldsByType = async ( const matchingDataStreams = pkg.data_streams.filter( (dataStream) => dataStream.type === dataStreamType ); - matchingDataStreams.forEach((dataStream) => - acc.push(loadFieldsFromYaml(pkg, dataStream.path)) - ); + matchingDataStreams.forEach((dataStream) => { + acc.push(loadFieldsFromYaml(pkg, dataStream.path)); + }); } return acc; }, []); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 4d36c3919198cc..05f552b558205b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -82,7 +82,8 @@ export async function _installPackage({ const installIndexPatternPromise = installIndexPatterns( savedObjectsClient, pkgName, - pkgVersion + pkgVersion, + installSource ).catch((reason) => (installIndexPatternError = reason)); const kibanaAssets = await getKibanaAssets(paths); if (installedPkg) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 770f342c0a6e78..33913b7f0c14de 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -21,7 +21,7 @@ export function getAssets( ): string[] { const assets: string[] = []; const { name, version } = packageInfo; - const paths = getArchiveFilelist({ name, version, installSource: 'registry' }); + const paths = getArchiveFilelist({ name, version }); // TODO: might be better to throw a PackageCacheError here if (!paths || paths.length === 0) return assets; 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 3df2d39419ab89..c10b26cbf0bd1a 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -7,8 +7,8 @@ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; import { isPackageLimited, installationStatuses } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { ArchivePackage, InstallSource, RegistryPackage, ValueOf } from '../../../../common/types'; -import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; +import { ArchivePackage, InstallSource, RegistryPackage } from '../../../../common/types'; +import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; import { getArchivePackage } from '../archive'; @@ -84,26 +84,6 @@ export async function getPackageSavedObjects( }); } -export async function getPackageKeysByStatus( - savedObjectsClient: SavedObjectsClientContract, - status: ValueOf<InstallationStatus> -) { - const allPackages = await getPackages({ savedObjectsClient, experimental: true }); - return allPackages.reduce<Array<{ pkgName: string; pkgVersion: string }>>((acc, pkg) => { - if (pkg.status === status) { - if (pkg.status === installationStatuses.Installed) { - // if we're looking for installed packages grab the version from the saved object because `getPackages` will - // return the latest package information from the registry - acc.push({ pkgName: pkg.name, pkgVersion: pkg.savedObject.attributes.version }); - } else { - acc.push({ pkgName: pkg.name, pkgVersion: pkg.version }); - } - } - - return acc; - }, []); -} - export async function getPackageInfo(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -139,7 +119,10 @@ export async function getPackageFromSource(options: { pkgName: string; pkgVersion: string; pkgInstallSource?: InstallSource; -}): Promise<{ paths: string[] | undefined; packageInfo: RegistryPackage | ArchivePackage }> { +}): Promise<{ + paths: string[] | undefined; + packageInfo: RegistryPackage | ArchivePackage; +}> { const { pkgName, pkgVersion, pkgInstallSource } = options; // TODO: Check package storage before checking registry let res; @@ -147,14 +130,16 @@ export async function getPackageFromSource(options: { 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; + if (!res.packageInfo || !res.paths) + throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); + return { + paths: res.paths, + packageInfo: res.packageInfo, + }; } export async function getInstallationObject(options: { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index e73a5d35338289..29300818288b42 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -313,7 +313,6 @@ async function installPackageByUpload({ setPackageInfo({ name: packageInfo.name, version: packageInfo.version, - installSource, packageInfo, }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index ca84980107fe36..2e879be20c18b7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -66,7 +66,6 @@ export async function removeInstallation(options: { deletePackageCache({ name: pkgName, version: pkgVersion, - installSource: installation.install_source, }); // successful delete's in SO client return {}. return something more useful diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 2d496055df78ae..d8368f2a46d190 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -128,11 +128,10 @@ export async function fetchCategories(params?: CategoriesParams): Promise<Catego } export async function getInfo(name: string, version: string) { - const installSource = 'registry'; - let packageInfo = getPackageInfo({ name, version, installSource }); + let packageInfo = getPackageInfo({ name, version }); if (!packageInfo) { packageInfo = await fetchInfo(name, version); - setPackageInfo({ name, version, packageInfo, installSource }); + setPackageInfo({ name, version, packageInfo }); } return packageInfo as RegistryPackage; } @@ -142,7 +141,7 @@ export async function getRegistryPackage( version: string ): Promise<{ paths: string[]; packageInfo: RegistryPackage }> { const installSource = 'registry'; - let paths = getArchiveFilelist({ name, version, installSource }); + let paths = getArchiveFilelist({ name, version }); if (!paths || paths.length === 0) { const { archiveBuffer, archivePath } = await fetchArchiveBuffer(name, version); paths = await unpackBufferToCache({ @@ -172,7 +171,7 @@ export async function ensureCachedArchiveInfo( version: string, installSource: InstallSource = 'registry' ) { - const paths = getArchiveFilelist({ name, version, installSource }); + const paths = getArchiveFilelist({ name, version }); if (!paths || paths.length === 0) { if (installSource === 'registry') { await getRegistryPackage(name, version); diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 7a62c307973c2a..d9015c51955362 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/global_search/server/routes/integration_tests/find.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts index 01bd68ca38b128..ed28786782c354 100644 --- a/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts +++ b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts @@ -33,7 +33,9 @@ const expectedResults = (...ids: string[]) => ids.map((id) => expect.objectConta describe('POST /internal/global_search/find', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; - let globalSearchHandlerContext: ReturnType<typeof globalSearchPluginMock.createRouteHandlerContext>; + let globalSearchHandlerContext: ReturnType< + typeof globalSearchPluginMock.createRouteHandlerContext + >; beforeEach(async () => { ({ server, httpSetup } = await setupServer(pluginId)); diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 385ce91d8f9810..8acbda5e0a6d46 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -173,9 +173,9 @@ describe('applicationResultProvider', () => { // test scheduler doesnt play well with promises. need to workaround by passing // an observable instead. Behavior with promise is asserted in previous tests of the suite - const applicationPromise = (hot('a', { a: application }) as unknown) as Promise< - ApplicationStart - >; + const applicationPromise = (hot('a', { + a: application, + }) as unknown) as Promise<ApplicationStart>; const provider = createApplicationResultProvider(applicationPromise); @@ -198,9 +198,9 @@ describe('applicationResultProvider', () => { // test scheduler doesnt play well with promises. need to workaround by passing // an observable instead. Behavior with promise is asserted in previous tests of the suite - const applicationPromise = (hot('a', { a: application }) as unknown) as Promise< - ApplicationStart - >; + const applicationPromise = (hot('a', { + a: application, + }) as unknown) as Promise<ApplicationStart>; const provider = createApplicationResultProvider(applicationPromise); diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index 135b9744765c60..9b9a964bdf71ad 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -164,9 +164,9 @@ describe('settings', () => { }); it('should set advanced settings', () => { - input('Sample size').prop('onChange')!({ target: { valueAsNumber: 13 } } as React.ChangeEvent< - HTMLInputElement - >); + input('Sample size').prop('onChange')!({ + target: { valueAsNumber: 13 }, + } as React.ChangeEvent<HTMLInputElement>); expect(dispatchSpy).toHaveBeenCalledWith( updateSettings( diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx index d0636da8cf93aa..1f25a53daf1ba3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx @@ -15,9 +15,9 @@ interface MatchParams { componentTemplateName?: string; } -export const ComponentTemplateListContainer: React.FunctionComponent<RouteComponentProps< - MatchParams ->> = ({ +export const ComponentTemplateListContainer: React.FunctionComponent< + RouteComponentProps<MatchParams> +> = ({ match: { params: { componentTemplateName }, }, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 39c4a2885efa5c..3902337f28ad23 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -45,9 +45,10 @@ interface Props { } export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Props) => { - const { parsedDefaultValue, multipleMappingsDeclared } = useMemo< - MappingsEditorParsedMetadata - >(() => { + const { + parsedDefaultValue, + multipleMappingsDeclared, + } = useMemo<MappingsEditorParsedMetadata>(() => { const mappingsDefinition = extractMappingsDefinition(value); if (mappingsDefinition === null) { diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index 43d84497af9e97..a880996daaade3 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -134,7 +134,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re columnConfigurations={columnConfigurations} items={streamItems} scale="medium" - wrap={false} + wrap={true} isReloading={isReloading} isLoadingMore={isLoadingMore} hasMoreBeforeStart={hasMoreBefore} diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_tooltip.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_tooltip.tsx index ccd207129e471d..e76e03279f73f9 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_tooltip.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_tooltip.tsx @@ -11,10 +11,9 @@ import { missingMlSetupPrivilegesDescription, } from './missing_privileges_messages'; -export const MissingSetupPrivilegesToolTip: React.FC<Omit< - PropsOf<EuiToolTip>, - 'content' | 'title' ->> = (props) => ( +export const MissingSetupPrivilegesToolTip: React.FC< + Omit<PropsOf<EuiToolTip>, 'content' | 'title'> +> = (props) => ( <EuiToolTip {...props} content={missingMlSetupPrivilegesDescription} diff --git a/x-pack/plugins/infra/public/hooks/use_kibana.ts b/x-pack/plugins/infra/public/hooks/use_kibana.ts index 24511014d1a065..af178493a5f2a0 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana.ts +++ b/x-pack/plugins/infra/public/hooks/use_kibana.ts @@ -20,6 +20,4 @@ export const createKibanaContextForPlugin = (core: CoreStart, pluginsStart: Infr ...pluginsStart, }); -export const useKibanaContextForPlugin = useKibana as () => KibanaReactContextValue< - PluginKibanaContextValue ->; +export const useKibanaContextForPlugin = useKibana as () => KibanaReactContextValue<PluginKibanaContextValue>; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts index 0a12c433db60a2..9f193d796e8e86 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts @@ -37,9 +37,10 @@ export const useLogEntryCategoriesResults = ({ }) => { const { services } = useKibanaContextForPlugin(); const [topLogEntryCategories, setTopLogEntryCategories] = useState<TopLogEntryCategories>([]); - const [logEntryCategoryDatasets, setLogEntryCategoryDatasets] = useState< - LogEntryCategoryDatasets - >([]); + const [ + logEntryCategoryDatasets, + setLogEntryCategoryDatasets, + ] = useState<LogEntryCategoryDatasets>([]); const [getTopLogEntryCategoriesRequest, getTopLogEntryCategories] = useTrackedPromise( { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts index e626e7477f2c05..396c1ad3e1857c 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts @@ -284,9 +284,10 @@ export const useLogEntryAnomaliesResults = ({ ); // Anomalies datasets - const [logEntryAnomaliesDatasets, setLogEntryAnomaliesDatasets] = useState< - LogEntryAnomaliesDatasets - >([]); + const [ + logEntryAnomaliesDatasets, + setLogEntryAnomaliesDatasets, + ] = useState<LogEntryAnomaliesDatasets>([]); const [getLogEntryAnomaliesDatasetsRequest, getLogEntryAnomaliesDatasets] = useTrackedPromise( { 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 9ca6db40a30549..32812f19a2541e 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 b56ede19743939..14785f64cffac0 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 3a52bb6b6ce710..b31afba8ac9cc3 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 4dec552c5bd6c6..7c3918c37ebbf5 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 0af8e01d7290d1..cf3752e649600b 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/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index e7568147dc568b..f1451c0bb3006b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -35,10 +35,9 @@ export function prependDatasourceExpression( if (datasourceExpressions.length === 0 || visualizationExpression === null) { return null; } - const parsedDatasourceExpressions: Array<[ - string, - Ast - ]> = datasourceExpressions.map(([layerId, expr]) => [ + const parsedDatasourceExpressions: Array< + [string, Ast] + > = datasourceExpressions.map(([layerId, expr]) => [ layerId, typeof expr === 'string' ? fromExpression(expr) : expr, ]); 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 647c0f3ac9cca7..0c96fc45de1284 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 00cb932a6d4e21..95aeedbd857cad 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 cd196745f3315d..e5c05a1cf8c7a9 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 b2edc61a56736d..2e57ecee860334 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/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 94018bd84b5174..2444a6a81c2a09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -18,15 +18,11 @@ import { DimensionEditor } from './dimension_editor'; import { DateRange } from '../../../common'; import { getOperationSupportMatrix } from './operation_support'; -export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< - IndexPatternPrivateState -> & { +export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps<IndexPatternPrivateState> & { uniqueLabel: string; }; -export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< - IndexPatternPrivateState -> & { +export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps<IndexPatternPrivateState> & { uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; 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 31fb5277d53ec4..817fdf637f0010 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/format_column.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.test.ts new file mode 100644 index 00000000000000..8d24ef4e86f19b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; +import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; +import { FormatColumnArgs, formatColumn } from './format_column'; + +describe('format_column', () => { + const fn: (input: Datatable, args: FormatColumnArgs) => Datatable = functionWrapper(formatColumn); + + let datatable: Datatable; + + beforeEach(() => { + datatable = { + type: 'datatable', + rows: [], + columns: [ + { + id: 'test', + name: 'test', + meta: { + type: 'number', + params: { + id: 'number', + }, + }, + }, + ], + }; + }); + + it('overwrites format', () => { + datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; + const result = fn(datatable, { columnId: 'test', format: 'otherformatter' }); + expect(result.columns[0].meta.params).toEqual({ + id: 'otherformatter', + }); + }); + + it('overwrites format with well known pattern', () => { + datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; + const result = fn(datatable, { columnId: 'test', format: 'number' }); + expect(result.columns[0].meta.params).toEqual({ + id: 'number', + params: { + pattern: '0,0.00', + }, + }); + }); + + it('uses number of decimals if provided', () => { + datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; + const result = fn(datatable, { columnId: 'test', format: 'number', decimals: 5 }); + expect(result.columns[0].meta.params).toEqual({ + id: 'number', + params: { + pattern: '0,0.00000', + }, + }); + }); + + it('has special handling for 0 decimals', () => { + datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; + const result = fn(datatable, { columnId: 'test', format: 'number', decimals: 0 }); + expect(result.columns[0].meta.params).toEqual({ + id: 'number', + params: { + pattern: '0,0', + }, + }); + }); + + describe('parent format', () => { + it('should ignore parent format if it is not specifying an id', () => { + const result = fn(datatable, { + columnId: 'test', + format: '', + parentFormat: JSON.stringify({ some: 'key' }), + }); + expect(result.columns[0].meta.params).toEqual(datatable.columns[0].meta.params); + }); + + it('set parent format with params', () => { + const result = fn(datatable, { + columnId: 'test', + format: '', + parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'wrapper', + params: { + wrapperParam: 123, + id: 'number', + }, + }); + }); + + it('retain inner formatter params', () => { + datatable.columns[0].meta.params = { id: 'myformatter', params: { innerParam: 456 } }; + const result = fn(datatable, { + columnId: 'test', + format: '', + parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'wrapper', + params: { + wrapperParam: 123, + id: 'myformatter', + params: { + innerParam: 456, + }, + }, + }); + }); + + it('overwrite existing wrapper param', () => { + datatable.columns[0].meta.params = { + id: 'wrapper', + params: { wrapperParam: 0, id: 'myformatter', params: { innerParam: 456 } }, + }; + const result = fn(datatable, { + columnId: 'test', + format: '', + parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'wrapper', + params: { + wrapperParam: 123, + id: 'myformatter', + params: { + innerParam: 456, + }, + }, + }); + }); + + it('overwrites format with well known pattern including decimals', () => { + datatable.columns[0].meta.params = { + id: 'previousWrapper', + params: { id: 'myformatter', params: { innerParam: 456 } }, + }; + const result = fn(datatable, { + columnId: 'test', + format: 'number', + decimals: 5, + parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'wrapper', + params: { + wrapperParam: 123, + id: 'number', + params: { + pattern: '0,0.00000', + }, + }, + }); + }); + }); + + it('does not touch other column meta data', () => { + const extraColumn: DatatableColumn = { id: 'test2', name: 'test2', meta: { type: 'number' } }; + datatable.columns.push(extraColumn); + const result = fn(datatable, { columnId: 'test', format: 'number' }); + expect(result.columns[1]).toEqual(extraColumn); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts index 1f337298a03adb..cc4d8db720ba23 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts @@ -10,7 +10,7 @@ import { DatatableColumn, } from 'src/plugins/expressions/public'; -interface FormatColumn { +export interface FormatColumnArgs { format: string; columnId: string; decimals?: number; @@ -50,7 +50,7 @@ export const supportedFormats: Record< export const formatColumn: ExpressionFunctionDefinition< 'lens_format_column', Datatable, - FormatColumn, + FormatColumnArgs, Datatable > = { name: 'lens_format_column', @@ -77,7 +77,7 @@ export const formatColumn: ExpressionFunctionDefinition< }, }, inputTypes: ['datatable'], - fn(input, { format, columnId, decimals, parentFormat }: FormatColumn) { + fn(input, { format, columnId, decimals, parentFormat }: FormatColumnArgs) { return { ...input, columns: input.columns.map((col) => { 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 51d95245adb252..3cf9bdc3a92f17 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 94f240058d6189..2c64431867df0f 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 ccdefee62ad5c2..263b4646c9feb5 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 2c6f42668d8638..d0cbcee61db6f6 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 72dfe85dfc0e9d..f27fb8d4642f6b 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 bd8c4b46833962..fd3ca4319669ed 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 bd4b452a49e1d5..13bddc0c2ec269 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 e33fc681b25794..30f64929fc1afd 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 7d50c28b7465ad..558fab02ad0840 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 659390a42f261f..efac9c151a4353 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 522e951bfba34c..1b0452d18a79ce 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 5c067ebaf21e9f..0e7e125944e719 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 37a7ef8ee25631..96df72ba8b7c15 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 b1cb2312d5bb8f..d2456e1c8d3751 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 ddc473a5c588d0..7c69a70c093516 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 bba7bda308b729..e43c7bbd2f72ec 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 f0e02c7ff0faf5..3ad9a1e5b36749 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 e1a31dc274837c..0d103a766c23a7 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 f071df15421471..1495a876a2c8eb 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 00000000000000..c3f7dac03ada30 --- /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 8d489df3660887..58685fa494a046 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 ea7aa62054e5c4..5b66d4aae77abd 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 1e6fc5a5806b55..e4958da471417a 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 d0ea81d1351563..01b834610eb1ae 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/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 3ec3abdfd62609..670a66d87e6d5f 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -218,18 +218,18 @@ const removeInvalidAccessors: SavedObjectMigrationFn< if (newDoc.attributes.visualizationType === 'lnsXY') { const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {}; const xyState = newDoc.attributes.state.visualization; - (newDoc.attributes as LensDocShapePre710< - XYStatePost77 - >).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => { - const layerId = layer.layerId; - const datasource = datasourceLayers[layerId]; - return { - ...layer, - xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined, - splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined, - accessors: layer.accessors.filter((accessor) => !!datasource?.columns[accessor]), - }; - }); + (newDoc.attributes as LensDocShapePre710<XYStatePost77>).state.visualization.layers = xyState.layers.map( + (layer: XYLayerPre77) => { + const layerId = layer.layerId; + const datasource = datasourceLayers[layerId]; + return { + ...layer, + xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined, + splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined, + accessors: layer.accessors.filter((accessor) => !!datasource?.columns[accessor]), + }; + } + ); } return newDoc; }; 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 3150cb9975f216..ff39d91be7e4a1 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/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index c2a751c03ee132..451bbaecca7e16 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -47,9 +47,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ * This is useful for end to end tests where we remove the auto generated parts for comparisons * such as created_at, updated_at, and id. */ -export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = (): Partial< - ExceptionListItemSchema -> => ({ +export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = (): Partial<ExceptionListItemSchema> => ({ comments: [], created_by: ELASTIC_USER, description: DESCRIPTION, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 7371a9d16fd4d9..495f88b8806200 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -60,9 +60,7 @@ export const getTrustedAppsListSchemaMock = (): ExceptionListSchema => { * This is useful for end to end tests where we remove the auto generated parts for comparisons * such as created_at, updated_at, and id. */ -export const getExceptionResponseMockWithoutAutoGeneratedValues = (): Partial< - ExceptionListSchema -> => ({ +export const getExceptionResponseMockWithoutAutoGeneratedValues = (): Partial<ExceptionListSchema> => ({ created_by: ELASTIC_USER, description: DESCRIPTION, immutable: IMMUTABLE, diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index 2b123280474dfb..8d4b419190bb64 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -87,9 +87,9 @@ const importList = async ({ list_id, type, signal, -}: ApiParams & ImportListItemSchemaEncoded & ImportListItemQuerySchemaEncoded): Promise< - ListSchema -> => { +}: ApiParams & + ImportListItemSchemaEncoded & + ImportListItemQuerySchemaEncoded): Promise<ListSchema> => { const formData = new FormData(); formData.append('file', file as Blob); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx index 4d9579e9e4c00d..72e8b3494da245 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx @@ -47,9 +47,7 @@ class MockDynamicStyleProperty { const defaultProps = { iconPaletteId: 'filledShapes', onChange: () => {}, - styleProperty: (new MockDynamicStyleProperty() as unknown) as IDynamicStyleProperty< - IconDynamicOptions - >, + styleProperty: (new MockDynamicStyleProperty() as unknown) as IDynamicStyleProperty<IconDynamicOptions>, isCustomOnly: false, }; diff --git a/x-pack/plugins/maps/public/components/action_select.tsx b/x-pack/plugins/maps/public/components/action_select.tsx index ad61a6a129974c..8ea9334bba7533 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.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 955a229d421a34..9a5110a0c24d24 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'; @@ -40,6 +40,7 @@ interface Props { backgroundColor: string; getFilterActions?: () => Promise<Action[]>; getActionContext?: () => ActionExecutionContext; + onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; areLayersLoaded: boolean; cancelAllInFlightRequests: () => void; exitFullScreen: () => void; @@ -191,6 +192,7 @@ export class MapContainer extends Component<Props, State> { addFilters, getFilterActions, getActionContext, + onSingleValueTrigger, flyoutDisplay, isFullScreen, exitFullScreen, @@ -250,6 +252,7 @@ export class MapContainer extends Component<Props, State> { addFilters={addFilters} getFilterActions={getFilterActions} getActionContext={getActionContext} + onSingleValueTrigger={onSingleValueTrigger} geoFields={this.state.geoFields} renderTooltipContent={renderTooltipContent} /> 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 edd501f2666907..97b47358ec089c 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 8547219b42e30d..60d9e57d15e23a 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 04c376a093623b..0ea40f6e3182f4 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 b178eef6fa5d39..c5c3ad4d78f7e7 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 ca4864f79940ee..4983e394ed93cc 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 caf21431145d56..7aaabc427790af 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/trigger_actions/trigger_utils.ts b/x-pack/plugins/maps/public/trigger_actions/trigger_utils.ts new file mode 100644 index 00000000000000..3505588a9c0497 --- /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/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx index 310cd4e3b3a79c..6039b29289d482 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -65,9 +65,10 @@ interface Props { } export const SupportedFieldsMessage: FC<Props> = ({ jobType }) => { - const [sourceIndexContainsSupportedFields, setSourceIndexContainsSupportedFields] = useState< - boolean - >(true); + const [ + sourceIndexContainsSupportedFields, + setSourceIndexContainsSupportedFields, + ] = useState<boolean>(true); const [sourceIndexFieldsCheckFailed, setSourceIndexFieldsCheckFailed] = useState<boolean>(false); const { fields } = newJobCapsService; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx index 12813ad6277aaf..83c2d86afca8be 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx @@ -33,9 +33,8 @@ describe('AnomalySwimlaneEmbeddableFactory', () => { } as AnomalySwimlaneEmbeddableInput); // assert - const mockCalls = ((AnomalySwimlaneEmbeddable as unknown) as jest.Mock< - AnomalySwimlaneEmbeddable - >).mock.calls[0]; + const mockCalls = ((AnomalySwimlaneEmbeddable as unknown) as jest.Mock<AnomalySwimlaneEmbeddable>) + .mock.calls[0]; const input = mockCalls[0]; const createServices = mockCalls[1]; diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 4ef905fd35fc4a..7ceb7b4945529b 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -99,28 +99,31 @@ function createLocalStateModule( ) { angular .module('monitoring/State', ['monitoring/Private']) - .service('globalState', function ( - Private: IPrivate, - $rootScope: ng.IRootScopeService, - $location: ng.ILocationService - ) { - function GlobalStateProvider(this: any) { - const state = new GlobalState(query, toasts, $rootScope, $location, this); - const initialState: any = state.getState(); - for (const key in initialState) { - if (!initialState.hasOwnProperty(key)) { - continue; + .service( + 'globalState', + function ( + Private: IPrivate, + $rootScope: ng.IRootScopeService, + $location: ng.ILocationService + ) { + function GlobalStateProvider(this: any) { + const state = new GlobalState(query, toasts, $rootScope, $location, this); + const initialState: any = state.getState(); + for (const key in initialState) { + if (!initialState.hasOwnProperty(key)) { + continue; + } + this[key] = initialState[key]; } - this[key] = initialState[key]; + this.save = () => { + const newState = { ...this }; + delete newState.save; + state.setState(newState); + }; } - this.save = () => { - const newState = { ...this }; - delete newState.save; - state.setState(newState); - }; + return Private(GlobalStateProvider); } - return Private(GlobalStateProvider); - }); + ); } function createMonitoringAppServices() { diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index dfe7280b717a3a..2c08354c9111f2 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 585a45cf5279c6..ea84a417c20eb1 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 6a05749df6d7a1..22867dde83a00d 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 00000000000000..34522ef95e27bd --- /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 7b9d7276dd1c56..9fdc59d61257ec 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 b635c2c68b9262..91d20d3478960c 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 343611294bc451..f60cab86453d14 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 8bce8205902fa5..f7fe3f5694a4a5 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 879d745ff2b649..b0710a5c695a7d 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 ef1820eaaeb3ec..be6df551663873 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 0c40ce0bf7a2ef..43f1072d06fc2d 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 55746ff6576a97..4819a0760d88aa 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 747ec8a441c427..32c6c6054f7752 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 00000000000000..3369765c68bd1e --- /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 00000000000000..79d58056af73c5 --- /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 8fdfc2bc622cad..f555f11be22518 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 91043a3da0dabb..7ee7db7ede17de 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/observability/public/hooks/use_has_data.ts b/x-pack/plugins/observability/public/hooks/use_has_data.ts new file mode 100644 index 00000000000000..9c66fa8861420c --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_has_data.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 { useContext } from 'react'; +import { HasDataContext } from '../context/has_data_context'; + +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 1b32933eec3e64..9774d9bed4244a 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 00000000000000..c89d52f904a96e --- /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 00000000000000..e8bed12aaa9bdf --- /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 00000000000000..2c06b7035f5156 --- /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 77b812dddd327e..a2a7cad1d5620e 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/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index 2d3142d4e5804c..f0c56eb7137e2f 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 d85bd1a624d7aa..87a836b2cb32c6 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); - const { data: alerts = [], status: alertStatus } = useFetcher(() => { - return getObservabilityAlerts({ core }); - }, [core]); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); + const relativeTime = { start: relativeStart, end: relativeEnd }; + const absoluteTime = { start: absoluteStart, end: absoluteEnd }; - const theme = useContext(ThemeContext); + const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); - const result = useFetcher( - () => fetchHasData(absoluteTime), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const hasData = result.data; + const { hasData, hasAnyData } = useHasData(); - 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 8713bb12292735..a28e34e7d4dcb2 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 64f5f4aab1c2bb..e3f8f877656bd2 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 cff6726e47df98..b1f8f0fb1bddc7 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 70c1eb1859ee3e..4cac1d586f295b 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,13 +37,13 @@ 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, @@ -54,7 +54,7 @@ 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/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 43243d265e926d..f0f72a0bc99657 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -9,10 +9,9 @@ import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; -export const createJobFnFactory: CreateJobFnFactory<CreateJobFn< - JobParamsCSV, - TaskPayloadCSV ->> = function createJobFactoryFn(reporting, parentLogger) { +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn<JobParamsCSV, TaskPayloadCSV> +> = function createJobFactoryFn(reporting, parentLogger) { const logger = parentLogger.clone([CSV_JOB_TYPE, 'create-job']); const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index 2c9ddea598eb48..6b4dd48583efec 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -10,9 +10,9 @@ import { decryptJobHeaders } from '../common'; import { createGenerateCsv } from './generate_csv'; import { TaskPayloadCSV } from './types'; -export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn< - TaskPayloadCSV ->> = function executeJobFactoryFn(reporting, parentLogger) { +export const runTaskFnFactory: RunTaskFnFactory< + RunTaskFn<TaskPayloadCSV> +> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); return async function runTask(jobId, job, cancellationToken) { diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 010b6f431db7ed..56ebcec2adf18d 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -10,10 +10,9 @@ import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPNG, TaskPayloadPNG } from '../types'; -export const createJobFnFactory: CreateJobFnFactory<CreateJobFn< - JobParamsPNG, - TaskPayloadPNG ->> = function createJobFactoryFn(reporting, parentLogger) { +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn<JobParamsPNG, TaskPayloadPNG> +> = function createJobFactoryFn(reporting, parentLogger) { const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute-job']); const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index e6b36643900dd4..2464b0f0687360 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -19,9 +19,9 @@ import { import { generatePngObservableFactory } from '../lib/generate_png'; import { TaskPayloadPNG } from '../types'; -export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn< - TaskPayloadPNG ->> = function executeJobFactoryFn(reporting, parentLogger) { +export const runTaskFnFactory: RunTaskFnFactory< + RunTaskFn<TaskPayloadPNG> +> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index a529cb864b6f73..c7010dbc40d108 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -10,10 +10,9 @@ import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPDF, TaskPayloadPDF } from '../types'; -export const createJobFnFactory: CreateJobFnFactory<CreateJobFn< - JobParamsPDF, - TaskPayloadPDF ->> = function createJobFactoryFn(reporting, parentLogger) { +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn<JobParamsPDF, TaskPayloadPDF> +> = function createJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([PDF_JOB_TYPE, 'create-job']); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 95716b8eab10fc..43aa8732920802 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -20,9 +20,9 @@ import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { getCustomLogo } from '../lib/get_custom_logo'; import { TaskPayloadPDF } from '../types'; -export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn< - TaskPayloadPDF ->> = function executeJobFactoryFn(reporting, parentLogger) { +export const runTaskFnFactory: RunTaskFnFactory< + RunTaskFn<TaskPayloadPDF> +> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); diff --git a/x-pack/plugins/runtime_fields/public/load_editor.tsx b/x-pack/plugins/runtime_fields/public/load_editor.tsx index f1b9c495f0336a..da2819411732bc 100644 --- a/x-pack/plugins/runtime_fields/public/load_editor.tsx +++ b/x-pack/plugins/runtime_fields/public/load_editor.tsx @@ -14,9 +14,9 @@ export interface OpenRuntimeFieldEditorProps { defaultValue?: RuntimeField; } -export const getRuntimeFieldEditorLoader = (coreSetup: CoreSetup) => async (): Promise< - LoadEditorResponse -> => { +export const getRuntimeFieldEditorLoader = ( + coreSetup: CoreSetup +) => async (): Promise<LoadEditorResponse> => { const { RuntimeFieldEditorFlyoutContent } = await import('./components'); const [core] = await coreSetup.getStartServices(); const { uiSettings, overlays, docLinks } = core; diff --git a/x-pack/plugins/saved_objects_tagging/public/plugin.test.ts b/x-pack/plugins/saved_objects_tagging/public/plugin.test.ts index 16dac754557109..cd14d70facf9b1 100644 --- a/x-pack/plugins/saved_objects_tagging/public/plugin.test.ts +++ b/x-pack/plugins/saved_objects_tagging/public/plugin.test.ts @@ -20,7 +20,9 @@ const MockedTagsCache = (TagsCache as unknown) as jest.Mock<PublicMethodsOf<Tags describe('SavedObjectTaggingPlugin', () => { let plugin: SavedObjectTaggingPlugin; let managementPluginSetup: ReturnType<typeof managementPluginMock.createSetupContract>; - let savedObjectsTaggingOssPluginSetup: ReturnType<typeof savedObjectTaggingOssPluginMock.createSetup>; + let savedObjectsTaggingOssPluginSetup: ReturnType< + typeof savedObjectTaggingOssPluginMock.createSetup + >; beforeEach(() => { const rawConfig: SavedObjectsTaggingClientConfigRawType = { diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index a0d63c0a9dd6f3..07e6ab6c72cb94 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 fd2b1cb8d1cf7e..77edd1a4ea8ddc 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 d253fed97f353e..6eb428adf2cd5f 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 d5c8d4e474c601..c22c5fc4ef0dad 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 8af75633776e89..64d456c3c6b0ad 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 e6d170122751ec..2b67f204848843 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 901d43adb659d5..e37d0024852d74 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 467b2a7ff99062..7110c8e130ac17 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 be152b21e27015..06469626842848 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/roles/edit_role/privileges/kibana/feature_table/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__fixtures__/index.ts index 09e1c0403a4037..0bac5b145003ee 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__fixtures__/index.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__fixtures__/index.ts @@ -37,16 +37,16 @@ export function getDisplayedFeaturePrivileges(wrapper: ReactWrapper<any>) { const subFeatureForm = featureControls.find(SubFeatureForm); if (subFeatureForm.length > 0) { - const independentPrivileges = (subFeatureForm.find(EuiCheckbox) as ReactWrapper< - EuiCheckboxProps - >).reduce((acc2, checkbox) => { + const independentPrivileges = (subFeatureForm.find( + EuiCheckbox + ) as ReactWrapper<EuiCheckboxProps>).reduce((acc2, checkbox) => { const { id: privilegeId, checked } = checkbox.props(); return checked ? [...acc2, privilegeId] : acc2; }, [] as string[]); - const mutuallyExclusivePrivileges = (subFeatureForm.find(EuiButtonGroup) as ReactWrapper< - EuiButtonGroupProps - >).reduce((acc2, subPrivButtonGroup) => { + const mutuallyExclusivePrivileges = (subFeatureForm.find( + EuiButtonGroup + ) as ReactWrapper<EuiButtonGroupProps>).reduce((acc2, subPrivButtonGroup) => { const { idSelected: selectedSubPrivilege } = subPrivButtonGroup.props(); return selectedSubPrivilege && selectedSubPrivilege !== 'none' ? [...acc2, selectedSubPrivilege] 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 4a2b86447b7f78..66b8002788dcbb 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 c22308fa8a43e0..e846539025452f 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/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index d16662922f6969..12491e934beae1 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -31,9 +31,9 @@ describe('Security Plugin', () => { const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext()); expect( plugin.setup( - coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup< - PluginStartDependencies - >, + coreMock.createSetup({ + basePath: '/some-base-path', + }) as CoreSetup<PluginStartDependencies>, { licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup(), diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index eef45598d1761c..718415e4857251 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 00000000000000..c296cb9c8e94d5 --- /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 00000000000000..6f02cce371a413 --- /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 048afb6190d18c..cfa9e715050669 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 76a6586e5af803..a306e701e4e8d2 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 f44c68588fd619..b46c8dc2178a40 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 b90a44be7aade1..11b2cdcac021b5 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 f72facb2e24cc9..93d43d04a86ca3 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 6be51d2a1adc23..26d2a2cff2910b 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 08c544b9246e0c..1bf6b64db24273 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 082f5100952abd..a4bdc4fc59a7cb 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 66ba15431e6031..e7d060b463aba6 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; }; }; }; @@ -816,9 +841,7 @@ export type ResolverEntityIndex = Array<{ entity_id: string }>; * `Type` types, we process the result of `TypeOf` instead, as this will be consistent. */ export type KbnConfigSchemaInputTypeOf<T> = T extends Record<string, unknown> - ? KbnConfigSchemaInputObjectTypeOf< - T - > /** `schema.number()` accepts strings, so this type should accept them as well. */ + ? KbnConfigSchemaInputObjectTypeOf<T> /** `schema.number()` accepts strings, so this type should accept them as well. */ : number extends T ? T | string : T; @@ -1068,7 +1091,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 3888d37a547f7a..967b3870cb9e00 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 fb1f2920aaceb1..596b92d064050f 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 eb8448233c6241..c2be6b2883c884 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/integration/value_lists.spec.ts b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts index 403538a37f5236..b97e1a874da7af 100644 --- a/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts @@ -148,7 +148,7 @@ describe('value lists', () => { it('deletes a "ip_range" from an uploaded file', () => { const listName = 'cidr_list.txt'; - importValueList(listName, 'ip_range'); + importValueList(listName, 'ip_range', ['192.168.100.0']); openValueListsModal(); deleteValueListsFile(listName); cy.get(VALUE_LISTS_TABLE) @@ -209,7 +209,7 @@ describe('value lists', () => { it('exports a "ip_range" list from an uploaded file', () => { const listName = 'cidr_list.txt'; - importValueList(listName, 'ip_range'); + importValueList(listName, 'ip_range', ['192.168.100.0']); openValueListsModal(); exportValueList(); cy.wait('@exportList').then((xhr) => { diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index bb32461a6bca2f..c249e0a77690c0 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -39,17 +39,17 @@ Cypress.Commands.add('stubSecurityApi', function (dataFileName) { cy.route('POST', 'api/solutions/security/graphql', `@${dataFileName}JSON`); }); -Cypress.Commands.add('stubSearchStrategyApi', function ( - dataFileName, - searchStrategyName = 'securitySolutionSearchStrategy' -) { - cy.on('window:before:load', (win) => { - win.fetch = null; - }); - cy.server(); - cy.fixture(dataFileName).as(`${dataFileName}JSON`); - cy.route('POST', `internal/search/${searchStrategyName}`, `@${dataFileName}JSON`); -}); +Cypress.Commands.add( + 'stubSearchStrategyApi', + function (dataFileName, searchStrategyName = 'securitySolutionSearchStrategy') { + cy.on('window:before:load', (win) => { + win.fetch = null; + }); + cy.server(); + cy.fixture(dataFileName).as(`${dataFileName}JSON`); + cy.route('POST', `internal/search/${searchStrategyName}`, `@${dataFileName}JSON`); + } +); Cypress.Commands.add( 'attachFile', diff --git a/x-pack/plugins/security_solution/cypress/tasks/lists.ts b/x-pack/plugins/security_solution/cypress/tasks/lists.ts index 1ecfeaad06d465..2ca4fa21e66504 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/lists.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/lists.ts @@ -77,27 +77,104 @@ export const deleteValueList = (list: string): Cypress.Chainable<Cypress.Respons }; /** - * Imports a single value list file this using Cypress Request and lists REST API + * Uploads list items using Cypress Request and lists REST API. + * + * This also will remove any upload data such as empty strings that can happen from the fixture + * due to extra lines being added from formatters such as prettier. + * @param file The file name to import + * @param type The type of the file import such as ip/keyword/text etc... + * @param data The contents of the file + * @param testSuggestions The type of test to use rather than the fixture file which is useful for ranges * Ref: https://www.elastic.co/guide/en/security/current/lists-api-import-list-items.html */ -export const importValueList = ( +export const uploadListItemData = ( file: string, - type: string + type: string, + data: string ): Cypress.Chainable<Cypress.Response> => { - return cy.fixture(file).then((data) => { - return cy.request({ - method: 'POST', - url: `api/lists/items/_import?type=${type}`, - encoding: 'binary', - headers: { - 'kbn-xsrf': 'upload-value-lists', - 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryJLrRH89J8QVArZyv', - }, - body: `------WebKitFormBoundaryJLrRH89J8QVArZyv\nContent-Disposition: form-data; name="file"; filename="${file}"\n\n${data}`, - }); + const removedEmptyLines = data + .split('\n') + .filter((line) => line.trim() !== '') + .join('\n'); + + return cy.request({ + method: 'POST', + url: `api/lists/items/_import?type=${type}`, + encoding: 'binary', + headers: { + 'kbn-xsrf': 'upload-value-lists', + 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryJLrRH89J8QVArZyv', + }, + body: `------WebKitFormBoundaryJLrRH89J8QVArZyv\nContent-Disposition: form-data; name="file"; filename="${file}"\n\n${removedEmptyLines}`, + }); +}; + +/** + * Checks a single value list file against a data set to ensure it has been uploaded. + * + * You can optionally pass in an array of test suggestions which will be useful for if you are + * using a range such as a CIDR range and need to ensure that test range has been added to the + * list but you cannot run an explicit test against that range. + * + * This also will remove any upload data such as empty strings that can happen from the fixture + * due to extra lines being added from formatters. + * @param file The file that was imported + * @param data The contents to check unless testSuggestions is given. + * @param type The type of the file import such as ip/keyword/text etc... + * @param testSuggestions The type of test to use rather than the fixture file which is useful for ranges + * Ref: https://www.elastic.co/guide/en/security/current/lists-api-import-list-items.html + */ +export const checkListItemData = ( + file: string, + data: string, + testSuggestions: string[] | undefined +): Cypress.Chainable<JQuery<HTMLElement>> => { + const importCheckLines = + testSuggestions == null + ? data.split('\n').filter((line) => line.trim() !== '') + : testSuggestions; + + return cy.wrap(importCheckLines).each((line) => { + return cy + .request({ + retryOnStatusCodeFailure: true, + method: 'GET', + url: `api/lists/items?list_id=${file}&value=${line}`, + }) + .then((resp) => { + expect(resp.status).to.eq(200); + }); }); }; +/** + * Imports a single value list file this using Cypress Request and lists REST API. After it + * imports the data, it will re-check and ensure that the data is there before continuing to + * get us more deterministic. + * + * You can optionally pass in an array of test suggestions which will be useful for if you are + * using a range such as a CIDR range and need to ensure that test range has been added to the + * list but you cannot run an explicit test against that range. + * + * This also will remove any upload data such as empty strings that can happen from the fixture + * due to extra lines being added from formatters. + * @param file The file to import + * @param type The type of the file import such as ip/keyword/text etc... + * @param testSuggestions The type of test to use rather than the fixture file which is useful for ranges + * Ref: https://www.elastic.co/guide/en/security/current/lists-api-import-list-items.html + */ +export const importValueList = ( + file: string, + type: string, + testSuggestions: string[] | undefined = undefined +): Cypress.Chainable<JQuery<HTMLElement>> => { + return cy + .fixture<string>(file) + .then((data) => uploadListItemData(file, type, data)) + .fixture<string>(file) + .then((data) => checkListItemData(file, data, testSuggestions)); +}; + /** * If you are on the value lists from the UI, this will loop over all the HTML elements * that have action-delete-value-list-${list_name} and delete all of those value lists 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 6b1f3699d333a7..dd01159e3029fa 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 c54bd8b621d835..859ba3d1a0951d 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 eb11357cd7ce94..5f244d64701fe4 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/settings/resilient/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx index f3aa48c765d3c8..0791219ef95f90 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx @@ -24,9 +24,9 @@ import * as i18n from './translations'; import { ConnectorTypes, ResilientFieldsType } from '../../../../../../case/common/api/connectors'; import { ConnectorCard } from '../card'; -const ResilientSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps< - ResilientFieldsType ->> = ({ isEdit = true, fields, connector, onChange }) => { +const ResilientSettingFieldsComponent: React.FunctionComponent< + SettingFieldsProps<ResilientFieldsType> +> = ({ isEdit = true, fields, connector, onChange }) => { const { incidentTypes = null, severityCode = null } = fields ?? {}; const { http, notifications } = useKibana().services; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx index 8b2e24628a760a..e5f9170f1fe392 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx @@ -27,9 +27,9 @@ const selectOptions = [ }, ]; -const ServiceNowSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps< - ServiceNowFieldsType ->> = ({ isEdit = true, fields, connector, onChange }) => { +const ServiceNowSettingFieldsComponent: React.FunctionComponent< + SettingFieldsProps<ServiceNowFieldsType> +> = ({ isEdit = true, fields, connector, onChange }) => { const { severity = null, urgency = null, impact = null } = fields ?? {}; const listItems = useMemo( 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 de3e9c07ae8a38..228f3a4319c338 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 0d5bf13cd62618..0d2df7c2de3ea0 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 83ee10e9b45a82..6046c3716b3b5b 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 c2ddcce8b1d3ce..b9db356498a01b 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 773d4b8d1fe56d..39ee21f942cbd0 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 e6cb8a9c3d150e..cd3827a2887fb6 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 2ae621e71a7252..9ca9cd6cce3893 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 7b6e9fb21a3e33..35cb8f7b1c91f7 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 c3c7c864ac99b7..bafe3df1a9cc7e 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 074e6faf80c7d9..a2a7182a768ccc 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 77d0ec330476c5..0acf461828bc37 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 bb74935d5703eb..4730dc5c2264f1 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 00000000000000..ad332b2759048c --- /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 421b111d7941fd..186083f1b05cdb 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 a4f2b0536abf5c..58f81c9fb3c8bb 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/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 2a778cd6585a14..087155ee5227eb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -43,12 +43,12 @@ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_asy describe('When the add exception modal is opened', () => { const ruleName = 'test rule'; - let defaultEndpointItems: jest.SpyInstance<ReturnType< - typeof helpers.defaultEndpointExceptionItems - >>; - let ExceptionBuilderComponent: jest.SpyInstance<ReturnType< - typeof builder.ExceptionBuilderComponent - >>; + let defaultEndpointItems: jest.SpyInstance< + ReturnType<typeof helpers.defaultEndpointExceptionItems> + >; + let ExceptionBuilderComponent: jest.SpyInstance< + ReturnType<typeof builder.ExceptionBuilderComponent> + >; beforeEach(() => { defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 9d017b7f1891e8..6beb941535b905 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -40,9 +40,9 @@ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_asy describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; - let ExceptionBuilderComponent: jest.SpyInstance<ReturnType< - typeof builder.ExceptionBuilderComponent - >>; + let ExceptionBuilderComponent: jest.SpyInstance< + ReturnType<typeof builder.ExceptionBuilderComponent> + >; beforeEach(() => { ExceptionBuilderComponent = jest diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 3de949a05c8816..5fe8e1fb48d1a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -37,16 +37,16 @@ mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('useAddOrUpdateException', () => { let updateAlertStatus: jest.SpyInstance<ReturnType<typeof alertsApi.updateAlertStatus>>; let addExceptionListItem: jest.SpyInstance<ReturnType<typeof listsApi.addExceptionListItem>>; - let updateExceptionListItem: jest.SpyInstance<ReturnType< - typeof listsApi.updateExceptionListItem - >>; + let updateExceptionListItem: jest.SpyInstance< + ReturnType<typeof listsApi.updateExceptionListItem> + >; let getQueryFilter: jest.SpyInstance<ReturnType<typeof getQueryFilterHelper.getQueryFilter>>; - let buildAlertStatusFilter: jest.SpyInstance<ReturnType< - typeof buildFilterHelpers.buildAlertStatusFilter - >>; - let buildAlertsRuleIdFilter: jest.SpyInstance<ReturnType< - typeof buildFilterHelpers.buildAlertsRuleIdFilter - >>; + let buildAlertStatusFilter: jest.SpyInstance< + ReturnType<typeof buildFilterHelpers.buildAlertStatusFilter> + >; + let buildAlertsRuleIdFilter: jest.SpyInstance< + ReturnType<typeof buildFilterHelpers.buildAlertsRuleIdFilter> + >; let addOrUpdateItemsArgs: Parameters<AddOrUpdateExceptionItemsFunc>; let render: () => RenderHookResult<UseAddOrUpdateExceptionProps, ReturnUseAddOrUpdateException>; const onError = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index f20a58b9ffa36a..9d2130069644c4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -27,9 +27,9 @@ describe('useFetchOrCreateRuleExceptionList', () => { let fetchRuleById: jest.SpyInstance<ReturnType<typeof rulesApi.fetchRuleById>>; let patchRule: jest.SpyInstance<ReturnType<typeof rulesApi.patchRule>>; let addExceptionList: jest.SpyInstance<ReturnType<typeof listsApi.addExceptionList>>; - let addEndpointExceptionList: jest.SpyInstance<ReturnType< - typeof listsApi.addEndpointExceptionList - >>; + let addEndpointExceptionList: jest.SpyInstance< + ReturnType<typeof listsApi.addEndpointExceptionList> + >; let fetchExceptionListById: jest.SpyInstance<ReturnType<typeof listsApi.fetchExceptionListById>>; let render: ( listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType'] diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index 6f04226fa3a198..272d40a8cea2b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -278,15 +278,9 @@ export const getMockPropsObj = ({ // silly that this needs to be an array and not an object // https://jestjs.io/docs/en/api#testeachtable-name-fn-timeout -export const testCases: Array<[ - LocationTypes, - string, - string, - string, - string | null, - string, - undefined | string -]> = [ +export const testCases: Array< + [LocationTypes, string, string, string, string | null, string, undefined | string] +> = [ [ /* page */ CONSTANTS.networkPage, /* namespaceLower */ 'network', diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index b6938bc18a88dc..5eec8cbbeb81f2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -48,9 +48,10 @@ export const useTimelineLastEventTime = ({ const refetch = useRef<inputsModel.Refetch>(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [TimelineLastEventTimeRequest, setTimelineLastEventTimeRequest] = useState< - TimelineEventsLastEventTimeRequestOptions - >({ + const [ + TimelineLastEventTimeRequest, + setTimelineLastEventTimeRequest, + ] = useState<TimelineEventsLastEventTimeRequestOptions>({ defaultIndex: indexNames, docValueFields, factoryQueryType: TimelineEventsQueries.lastEventTime, @@ -58,9 +59,10 @@ export const useTimelineLastEventTime = ({ details, }); - const [timelineLastEventTimeResponse, setTimelineLastEventTimeResponse] = useState< - UseTimelineLastEventTimeArgs - >({ + const [ + timelineLastEventTimeResponse, + setTimelineLastEventTimeResponse, + ] = useState<UseTimelineLastEventTimeArgs>({ lastSeen: null, refetch: refetch.current, errorMessage: undefined, diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index a2cf59314d1495..3ef6d78d651aab 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -61,9 +61,10 @@ export const useMatrixHistogram = ({ const refetch = useRef<inputsModel.Refetch>(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [matrixHistogramRequest, setMatrixHistogramRequest] = useState< - MatrixHistogramRequestOptions - >({ + const [ + matrixHistogramRequest, + setMatrixHistogramRequest, + ] = useState<MatrixHistogramRequestOptions>({ defaultIndex: indexNames, factoryQueryType: MatrixHistogramQuery, filterQuery: createFilter(filterQuery), 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 0944b6aa27f678..ba375612b22a71 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 ed226fb0c984fe..0118004b48eb8e 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 7246259f5afa1b..ac8c78b1fdbd4b 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 febcf0aee679df..5450a6ec1a3138 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 ecc0fc54d0d47a..6b7cc8167ede6d 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/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index 1cb13da2048ad4..7045ebb4c18383 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -44,9 +44,10 @@ export const useFirstLastSeenHost = ({ const { data, notifications } = useKibana().services; const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [firstLastSeenHostRequest, setFirstLastSeenHostRequest] = useState< - HostFirstLastSeenRequestOptions - >({ + const [ + firstLastSeenHostRequest, + setFirstLastSeenHostRequest, + ] = useState<HostFirstLastSeenRequestOptions>({ defaultIndex: indexNames, docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.firstLastSeen, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index 281f8489fce0fc..3564b9f4516d99 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -70,9 +70,10 @@ export const useHostsKpiAuthentications = ({ : null ); - const [hostsKpiAuthenticationsResponse, setHostsKpiAuthenticationsResponse] = useState< - HostsKpiAuthenticationsArgs - >({ + const [ + hostsKpiAuthenticationsResponse, + setHostsKpiAuthenticationsResponse, + ] = useState<HostsKpiAuthenticationsArgs>({ authenticationsSuccess: 0, authenticationsSuccessHistogram: [], authenticationsFailure: 0, diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index 18745897c594f4..d8b80c07f44d8d 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -170,8 +170,7 @@ const EndpointsEmptyState = React.memo<{ }, { title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', { - defaultMessage: - 'Enroll your agents enabled with Endpoint Security through Ingest Manager', + defaultMessage: 'Enroll your agents enabled with Endpoint Security through Fleet', }), status: actionDisabled ? 'disabled' : '', children: ( 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 84d1dabe869105..2e9206d945cad8 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 26d8dda2f4aec1..33772f4463543f 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 29d9185b6cea55..1901f3589104a5 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 ec22c522c3d0a9..63ec991ecf6d1a 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 00000000000000..ce6d2f354cc458 --- /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 00000000000000..6718dfe4cb9b48 --- /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 dd7475361b950b..02d41e7dea1a74 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,191 @@ 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', + { + path: getEndpointDetailsPath({ + name: 'endpointDetails', + selected_endpoint: details.agent.id, + }), + }, + ], }, - ], - }, - }); + } + ); - const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); + 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} + 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> + {isPolicyOutOfDate(details.Endpoint.policy.applied, policyInfo) && <OutOfDate />} + </> + ), + }, + { + 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 6bc3445c8e7458..edc15e22a699ee 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 4b955f2fe2959a..69889d3d0a881d 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 2b40a7507da886..a759c9de414156 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'; @@ -217,17 +219,18 @@ export const EndpointList = () => { const NOOP = useCallback(() => {}, []); - const handleDeployEndpointsClick = useNavigateToAppEventHandler< - AgentPolicyDetailsDeployAgentAction - >('fleet', { - path: `#/policies/${selectedPolicyId}?openEnrollmentFlyout=true`, - state: { - onDoneNavigateTo: [ - 'securitySolution:administration', - { path: getEndpointListPath({ name: 'endpointList' }) }, - ], - }, - }); + const handleDeployEndpointsClick = useNavigateToAppEventHandler<AgentPolicyDetailsDeployAgentAction>( + 'fleet', + { + path: `#/policies/${selectedPolicyId}?openEnrollmentFlyout=true`, + state: { + onDoneNavigateTo: [ + 'securitySolution:administration', + { path: getEndpointListPath({ name: 'endpointList' }) }, + ], + }, + } + ); const selectionOptions = useMemo<EuiSelectableProps['options']>(() => { return policyItems.map((item) => { @@ -322,17 +325,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_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index f7b1a8e901ed2c..cd2ef62c1cb843 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -25,11 +25,13 @@ export const WindowsEvents = React.memo(() => { const total = usePolicyDetailsSelector(totalWindowsEvents); const checkboxes = useMemo(() => { - const items: Immutable<Array<{ - name: string; - os: 'windows'; - protectionField: keyof UIPolicyConfig['windows']['events']; - }>> = [ + const items: Immutable< + Array<{ + name: string; + os: 'windows'; + protectionField: keyof UIPolicyConfig['windows']['events']; + }> + > = [ { name: i18n.translate( 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index c72093552f5511..c5724956bc21fa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -119,11 +119,13 @@ export const MalwareProtections = React.memo(() => { policyDetailsConfig && policyDetailsConfig.windows.popup.malware.message; const isPlatinumPlus = useLicense().isPlatinumPlus(); - const radios: Immutable<Array<{ - id: ProtectionModes; - label: string; - protection: 'malware'; - }>> = useMemo(() => { + const radios: Immutable< + Array<{ + id: ProtectionModes; + label: string; + protection: 'malware'; + }> + > = useMemo(() => { return [ { id: ProtectionModes.detect, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 5315087c09655e..75b06fb9b84320 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -26,9 +26,7 @@ export type TrustedAppsListResourceStateChanged = ResourceStateChanged< TrustedAppsListData >; -export type TrustedAppDeletionSubmissionResourceStateChanged = ResourceStateChanged< - 'trustedAppDeletionSubmissionResourceStateChanged' ->; +export type TrustedAppDeletionSubmissionResourceStateChanged = ResourceStateChanged<'trustedAppDeletionSubmissionResourceStateChanged'>; export type TrustedAppDeletionDialogStarted = Action<'trustedAppDeletionDialogStarted'> & { payload: { diff --git a/x-pack/plugins/security_solution/public/network/components/details/mock.ts b/x-pack/plugins/security_solution/public/network/components/details/mock.ts index b1187a9b22d942..7d5e8538939ea4 100644 --- a/x-pack/plugins/security_solution/public/network/components/details/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/details/mock.ts @@ -6,10 +6,9 @@ import { NetworkDetailsStrategyResponse } from '../../../../common/search_strategy'; -export const mockData: Readonly<Record< - string, - NetworkDetailsStrategyResponse['networkDetails'] ->> = { +export const mockData: Readonly< + Record<string, NetworkDetailsStrategyResponse['networkDetails']> +> = { complete: { source: { firstSeen: '2019-02-07T17:19:41.636Z', diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 3a4ce40fb8b808..0cce4842809063 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -74,9 +74,10 @@ export const useNetworkKpiNetworkEvents = ({ : null ); - const [networkKpiNetworkEventsResponse, setNetworkKpiNetworkEventsResponse] = useState< - NetworkKpiNetworkEventsArgs - >({ + const [ + networkKpiNetworkEventsResponse, + setNetworkKpiNetworkEventsResponse, + ] = useState<NetworkKpiNetworkEventsArgs>({ networkEvents: 0, id: ID, inspect: { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index 40cd04207b2d97..565504ca3ef09d 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -74,9 +74,10 @@ export const useNetworkKpiTlsHandshakes = ({ : null ); - const [networkKpiTlsHandshakesResponse, setNetworkKpiTlsHandshakesResponse] = useState< - NetworkKpiTlsHandshakesArgs - >({ + const [ + networkKpiTlsHandshakesResponse, + setNetworkKpiTlsHandshakesResponse, + ] = useState<NetworkKpiTlsHandshakesArgs>({ tlsHandshakes: 0, id: ID, inspect: { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index a2a64d0770558b..6924f3202076bd 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -74,9 +74,10 @@ export const useNetworkKpiUniqueFlows = ({ : null ); - const [networkKpiUniqueFlowsResponse, setNetworkKpiUniqueFlowsResponse] = useState< - NetworkKpiUniqueFlowsArgs - >({ + const [ + networkKpiUniqueFlowsResponse, + setNetworkKpiUniqueFlowsResponse, + ] = useState<NetworkKpiUniqueFlowsArgs>({ uniqueFlowId: 0, id: ID, inspect: { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index d183eb243e1585..0b14945bba9ff3 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -78,9 +78,10 @@ export const useNetworkKpiUniquePrivateIps = ({ : null ); - const [networkKpiUniquePrivateIpsResponse, setNetworkKpiUniquePrivateIpsResponse] = useState< - NetworkKpiUniquePrivateIpsArgs - >({ + const [ + networkKpiUniquePrivateIpsResponse, + setNetworkKpiUniquePrivateIpsResponse, + ] = useState<NetworkKpiUniquePrivateIpsArgs>({ uniqueDestinationPrivateIps: 0, uniqueDestinationPrivateIpsHistogram: null, uniqueSourcePrivateIps: 0, diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index 0676d5976e2115..fa9a6ac08e8129 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -111,9 +111,10 @@ export const useNetworkTopCountries = ({ [limit] ); - const [networkTopCountriesResponse, setNetworkTopCountriesResponse] = useState< - NetworkTopCountriesArgs - >({ + const [ + networkTopCountriesResponse, + setNetworkTopCountriesResponse, + ] = useState<NetworkTopCountriesArgs>({ networkTopCountries: [], id: queryId, inspect: { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index a79ffda0bcce92..9334e14af5ecd5 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -83,21 +83,22 @@ export const originID: (state: DataState) => string | undefined = createSelector /** * Process events that will be displayed as terminated. */ -export const terminatedProcesses = createSelector(resolverTreeResponse, function ( - tree?: ResolverTree -) { - if (!tree) { - return new Set(); +export const terminatedProcesses = createSelector( + resolverTreeResponse, + function (tree?: ResolverTree) { + if (!tree) { + return new Set(); + } + return new Set( + resolverTreeModel + .lifecycleEvents(tree) + .filter(isTerminatedProcess) + .map((terminatedEvent) => { + return eventModel.entityIDSafeVersion(terminatedEvent); + }) + ); } - return new Set( - resolverTreeModel - .lifecycleEvents(tree) - .filter(isTerminatedProcess) - .map((terminatedEvent) => { - return eventModel.entityIDSafeVersion(terminatedEvent); - }) - ); -}); +); /** * A function that given an entity id returns a boolean indicating if the id is in the set of terminated processes. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts index 1c4e1f4199bc48..714b103e5714de 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts @@ -7,11 +7,13 @@ import { deepObjectEntries } from './deep_object_entries'; describe('deepObjectEntries', () => { - const valuesAndExpected: Array<[ - objectValue: object, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expected: Array<[path: Array<keyof any>, fieldValue: unknown]> - ]> = [ + const valuesAndExpected: Array< + [ + objectValue: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expected: Array<[path: Array<keyof any>, fieldValue: unknown]> + ] + > = [ [{}, []], // No 'field' values found [{ a: {} }, []], // No 'field' values found [{ a: { b: undefined } }, []], // No 'field' values found diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index 5675e29fc2bc14..27a7723d7d6560 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -220,9 +220,9 @@ const NodeDetailView = memo(function ({ } as HTMLAttributes<HTMLElement> } descriptionProps={ - { 'data-test-subj': 'resolver:node-detail:entry-description' } as HTMLAttributes< - HTMLElement - > + { + 'data-test-subj': 'resolver:node-detail:entry-description', + } as HTMLAttributes<HTMLElement> } compressed listItems={processInfoEntry} 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 95ad5285507c5b..c163ab1ae448bd 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 7d0f5995afc3bc..f5ad6264f95e2d 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 f24ef3448d03f9..4a314d76a51bf6 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 3d2c42c33c9751..fed6a39ae2ed54 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 7528468ef65222..10eb1405158266 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 7192580f2426d3..00000000000000 --- 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 921527a0079e33..20faf93616a8c2 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 0afca363096595..a728e351220609 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 3d08d56d6fb195..e942dce7245201 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 576dedfc28b1b7..6fddb5403561e6 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 c6d4325f00739f..15d7d750257ac1 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 17dd83e9ab3f45..19d657b0537a5e 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 83e824aa2450a6..6c28c0ce16df1d 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 8fa5d18c0c4f50..99dfd53145e9f3 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 e1667ab949732d..05a66c6853f6cc 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 d7a05e39e76b2f..120b3ce165909d 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 00000000000000..4b595fad9be6f6 --- /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 b1f48608346c78..77aee2c4bf0126 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 19b360b24391de..a4c4679c820580 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 35d31e034e7f38..baa62b629567da 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 5e0d15f3bfbc3b..00000000000000 --- 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 b63359077bf2c9..00000000000000 --- 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 ae30f11d8bb168..00000000000000 --- 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 d146818e7ab907..e4c49ce197c2a4 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 7fc269c954ac40..900699503a3bb7 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 f7c76c110ac3f1..d5148eeb3655f5 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 50bf8b37adf28d..287fcd7f11e93d 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 5f92596f033114..2465d0a5364823 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 c066de8af9f209..c2fff49afdcbf7 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 ce469c2bf57a28..39174c9092af57 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 92a913c9c33758..78e30bd81817c2 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 9a0bf5ec4a940c..241b8c5030de79 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 ec4d37d3b70a25..7d015c1dc82b14 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 7bd86cd7e24527..cd89c9df7e3db5 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 7c227f1c806101..3f2b56b3f7dba9 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 11964ab4d7b283..58e2ea6111a38e 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 7a1a0f06a22678..61264a8848f61a 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>; @@ -57,9 +58,7 @@ export const createMockEndpointAppContextService = ( /** * Creates a mocked input contract for the `EndpointAppContextService#start()` method */ -export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< - EndpointAppContextServiceStartContract -> => { +export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<EndpointAppContextServiceStartContract> => { const factory = new AppClientFactory(); const config = createMockConfig(); factory.setup({ getSpaceId: () => 'mockSpace', config }); @@ -90,18 +89,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 +103,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 00000000000000..5dd668b857229e --- /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 f2011e99565c80..a79175b178c38b 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/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 1f90c689a688f2..25de64aac5258f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -69,9 +69,7 @@ describe('test endpoint route', () => { beforeEach(() => { mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked< - ILegacyClusterClient - >; + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked<ILegacyClusterClient>; mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts index 2c7d1e9e48404c..44776bfddd61a4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts @@ -63,9 +63,7 @@ describe('test endpoint route v1', () => { }; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked< - ILegacyClusterClient - >; + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked<ILegacyClusterClient>; mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); 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 ed3c48ed6c6770..e9a1f1e24fa555 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 cd273f785033c8..c88f11422d0f04 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 009ce043db85ed..0fc3f5135c8f6e 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/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index e670ca6e20cb24..dd4ade1906bc6f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -48,9 +48,10 @@ export async function getPolicyResponseByAgentId( dataClient: ILegacyScopedClusterClient ): Promise<GetHostPolicyResponse | undefined> { const query = getESQueryPolicyResponseByAgentID(agentID, index); - const response = (await dataClient.callAsCurrentUser('search', query)) as SearchResponse< - HostPolicyResponse - >; + const response = (await dataClient.callAsCurrentUser( + 'search', + query + )) as SearchResponse<HostPolicyResponse>; if (response.hits.hits.length === 0) { return undefined; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index 3be0cc5daff792..ab3f90644b51a3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -54,9 +54,9 @@ export class EventsQuery { if (kql) { kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); } - const response: ApiResponse<SearchResponse< - SafeResolverEvent - >> = await client.asCurrentUser.search(this.buildSearch(kqlQuery)); + const response: ApiResponse< + SearchResponse<SafeResolverEvent> + > = await client.asCurrentUser.search(this.buildSearch(kqlQuery)); return response.body.hits.hits.map((hit) => hit._source); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index f5a27b7602e8b3..9cb3f3d20543c7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -271,7 +271,9 @@ describe('manifest_manager', () => { }); test('ManifestManager can commit manifest', async () => { - const savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create> = savedObjectsClientMock.create(); + const savedObjectsClient: ReturnType< + typeof savedObjectsClientMock.create + > = savedObjectsClientMock.create(); const manifestManager = getManifestManagerMock({ savedObjectsClient, }); diff --git a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts index 10faa362363ad0..f4bbc9ab9189ec 100644 --- a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts @@ -15,9 +15,7 @@ export type QueryAllNoteResolver = AppResolverWithFields< 'totalCount' | 'Note' >; -export type QueryNotesByTimelineIdResolver = AppResolverOf< - QueryResolvers.GetNotesByTimelineIdResolver ->; +export type QueryNotesByTimelineIdResolver = AppResolverOf<QueryResolvers.GetNotesByTimelineIdResolver>; export type QueryNotesByEventIdResolver = AppResolverOf<QueryResolvers.GetNotesByEventIdResolver>; @@ -27,9 +25,7 @@ export type MutationNoteResolver = AppResolverOf< export type MutationDeleteNoteResolver = AppResolverOf<MutationResolvers.DeleteNoteResolver>; -export type MutationDeleteNoteByTimelineIdResolver = AppResolverOf< - MutationResolvers.DeleteNoteByTimelineIdResolver ->; +export type MutationDeleteNoteByTimelineIdResolver = AppResolverOf<MutationResolvers.DeleteNoteByTimelineIdResolver>; interface NoteResolversDeps { note: Note; diff --git a/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts index 49072f0279de82..a16db47aaa40b6 100644 --- a/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts @@ -8,21 +8,13 @@ import { AppResolverOf } from '../../lib/framework'; import { MutationResolvers, QueryResolvers } from '../types'; import { PinnedEvent } from '../../lib/pinned_event/saved_object'; -export type QueryAllPinnedEventsByTimelineIdResolver = AppResolverOf< - QueryResolvers.GetAllPinnedEventsByTimelineIdResolver ->; +export type QueryAllPinnedEventsByTimelineIdResolver = AppResolverOf<QueryResolvers.GetAllPinnedEventsByTimelineIdResolver>; -export type MutationPinnedEventResolver = AppResolverOf< - MutationResolvers.PersistPinnedEventOnTimelineResolver ->; +export type MutationPinnedEventResolver = AppResolverOf<MutationResolvers.PersistPinnedEventOnTimelineResolver>; -export type MutationDeletePinnedEventOnTimelineResolver = AppResolverOf< - MutationResolvers.DeletePinnedEventOnTimelineResolver ->; +export type MutationDeletePinnedEventOnTimelineResolver = AppResolverOf<MutationResolvers.DeletePinnedEventOnTimelineResolver>; -export type MutationDeleteAllPinnedEventsOnTimelineResolver = AppResolverOf< - MutationResolvers.DeleteAllPinnedEventsOnTimelineResolver ->; +export type MutationDeleteAllPinnedEventsOnTimelineResolver = AppResolverOf<MutationResolvers.DeleteAllPinnedEventsOnTimelineResolver>; interface TimelineResolversDeps { pinnedEvent: PinnedEvent; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index 9e6e9bf6ea22ec..fc14663b138b27 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -19,9 +19,7 @@ export type MutationTimelineResolver = AppResolverOf< MutationResolvers.PersistTimelineResolver<QueryTimelineResolver> >; -export type MutationDeleteTimelineResolver = AppResolverOf< - MutationResolvers.DeleteTimelineResolver ->; +export type MutationDeleteTimelineResolver = AppResolverOf<MutationResolvers.DeleteTimelineResolver>; export type MutationFavoriteResolver = AppResolverOf<MutationResolvers.PersistFavoriteResolver>; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index fd29be0e81f3c6..e1859a57a8f816 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -508,18 +508,14 @@ export const getMockPrivilegesResult = () => ({ application: {}, }); -export const getFindResultStatusEmpty = (): SavedObjectsFindResponse< - IRuleSavedAttributesSavedObjectAttributes -> => ({ +export const getFindResultStatusEmpty = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ page: 1, per_page: 1, total: 0, saved_objects: [], }); -export const getFindResultStatus = (): SavedObjectsFindResponse< - IRuleSavedAttributesSavedObjectAttributes -> => ({ +export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ page: 1, per_page: 6, total: 2, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 2ff6d6ac646ae6..5fa0bf525a5d9a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -24,13 +24,14 @@ export const createRuleActionsSavedObject = async ({ actions = [], throttle, }: CreateRuleActionsSavedObject): Promise<RulesActionsSavedObject> => { - const ruleActionsSavedObject = await savedObjectsClient.create< - IRuleActionsAttributesSavedObjectAttributes - >(ruleActionsSavedObjectType, { - ruleAlertId, - actions, - ...getThrottleOptions(throttle), - }); + const ruleActionsSavedObject = await savedObjectsClient.create<IRuleActionsAttributesSavedObjectAttributes>( + ruleActionsSavedObjectType, + { + ruleAlertId, + actions, + ...getThrottleOptions(throttle), + } + ); return getRuleActionsFromSavedObject(ruleActionsSavedObject); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index f469aa8634c5a5..f8b5626b09cd39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -26,10 +26,10 @@ export const getRuleActionsSavedObject = async ({ ruleAlertId, savedObjectsClient, }: GetRuleActionsSavedObject): Promise<RulesActionsSavedObject | null> => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects } = await savedObjectsClient.find< - IRuleActionsAttributesSavedObjectAttributes - >({ + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + saved_objects, + } = await savedObjectsClient.find<IRuleActionsAttributesSavedObjectAttributes>({ type: ruleActionsSavedObjectType, perPage: 1, search: `${ruleAlertId}`, diff --git a/x-pack/plugins/security_solution/server/lib/sources/configuration.ts b/x-pack/plugins/security_solution/server/lib/sources/configuration.ts index aa620f6cf2590c..2526199f9e07f6 100644 --- a/x-pack/plugins/security_solution/server/lib/sources/configuration.ts +++ b/x-pack/plugins/security_solution/server/lib/sources/configuration.ts @@ -17,9 +17,9 @@ export class ConfigurationSourcesAdapter implements SourcesAdapter { private readonly configuration: ConfigurationAdapter<ConfigurationWithSources>; constructor( - configuration: ConfigurationAdapter< - ConfigurationWithSources - > = new InmemoryConfigurationAdapter({ sources: {} }) + configuration: ConfigurationAdapter<ConfigurationWithSources> = new InmemoryConfigurationAdapter( + { sources: {} } + ) ) { this.configuration = configuration; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 8a33b1df4caa8a..d963b3b093d818 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/security_solution/server/usage/endpoints/endpoint.test.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts index 6d4b97564c90ef..5de14fd574be0d 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts @@ -20,9 +20,9 @@ import * as fleetSavedObjects from './fleet_saved_objects'; describe('test security solution endpoint telemetry', () => { let mockSavedObjectsRepository: jest.Mocked<ISavedObjectsRepository>; let getFleetSavedObjectsMetadataSpy: jest.SpyInstance<Promise<SavedObjectsFindResponse<Agent>>>; - let getLatestFleetEndpointEventSpy: jest.SpyInstance<Promise< - SavedObjectsFindResponse<AgentEventSOAttributes> - >>; + let getLatestFleetEndpointEventSpy: jest.SpyInstance< + Promise<SavedObjectsFindResponse<AgentEventSOAttributes>> + >; beforeAll(() => { getLatestFleetEndpointEventSpy = jest.spyOn(fleetSavedObjects, 'getLatestFleetEndpointEvent'); diff --git a/x-pack/plugins/spaces/public/management/management_service.test.ts b/x-pack/plugins/spaces/public/management/management_service.test.ts index 444ccf43d3d1f7..a2d045449dc563 100644 --- a/x-pack/plugins/spaces/public/management/management_service.test.ts +++ b/x-pack/plugins/spaces/public/management/management_service.test.ts @@ -22,9 +22,8 @@ describe('ManagementService', () => { managementMockSetup.sections.section.kibana = mockKibanaSection; const deps = { management: managementMockSetup, - getStartServices: coreMock.createSetup().getStartServices as CoreSetup< - PluginsStart - >['getStartServices'], + getStartServices: coreMock.createSetup() + .getStartServices as CoreSetup<PluginsStart>['getStartServices'], spacesManager: spacesManagerMock.create(), }; @@ -43,9 +42,8 @@ describe('ManagementService', () => { it('will not crash if the kibana section is missing', () => { const deps = { management: managementPluginMock.createSetupContract(), - getStartServices: coreMock.createSetup().getStartServices as CoreSetup< - PluginsStart - >['getStartServices'], + getStartServices: coreMock.createSetup() + .getStartServices as CoreSetup<PluginsStart>['getStartServices'], spacesManager: spacesManagerMock.create(), }; @@ -65,9 +63,8 @@ describe('ManagementService', () => { const deps = { management: managementMockSetup, - getStartServices: coreMock.createSetup().getStartServices as CoreSetup< - PluginsStart - >['getStartServices'], + getStartServices: coreMock.createSetup() + .getStartServices as CoreSetup<PluginsStart>['getStartServices'], spacesManager: spacesManagerMock.create(), }; diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 88adf98248d2c0..4fd95295073354 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -209,9 +209,9 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked< - SpacesClient - >; + const spacesClient = spacesService.createSpacesClient( + null as any + ) as jest.Mocked<SpacesClient>; spacesClient.getAll.mockImplementation(() => Promise.resolve([ { id: 'ns-1', name: '', disabledFeatures: [] }, @@ -240,9 +240,9 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked< - SpacesClient - >; + const spacesClient = spacesService.createSpacesClient( + null as any + ) as jest.Mocked<SpacesClient>; spacesClient.getAll.mockImplementation(() => Promise.resolve([ { id: 'ns-1', name: '', disabledFeatures: [] }, @@ -271,9 +271,9 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked< - SpacesClient - >; + const spacesClient = spacesService.createSpacesClient( + null as any + ) as jest.Mocked<SpacesClient>; spacesClient.getAll.mockImplementation(() => Promise.resolve([ { id: 'ns-1', name: '', disabledFeatures: [] }, diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index c573d3b738373c..a34b367668ec72 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -85,10 +85,9 @@ function validateQuery(query: Query) { return true; } -export const GeoThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeParamsExpressionProps< - GeoThresholdAlertParams, - AlertsContextValue ->> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { +export const GeoThresholdAlertTypeExpression: React.FunctionComponent< + AlertTypeParamsExpressionProps<GeoThresholdAlertParams, AlertsContextValue> +> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { const { index, indexId, diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 92cb8c9055bde4..274cd5a9d20dcf 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -66,10 +66,9 @@ const expressionFieldsWithValidation = [ 'timeWindowSize', ]; -export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeParamsExpressionProps< - IndexThresholdAlertParams, - AlertsContextValue ->> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { +export const IndexThresholdAlertTypeExpression: React.FunctionComponent< + AlertTypeParamsExpressionProps<IndexThresholdAlertParams, AlertsContextValue> +> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { const { index, timeField, diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index 5addad7086f698..3e0d517172d01e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -92,10 +92,9 @@ export function createTaskRunAggregator( ); const resultFrequencyQueue = createRunningAveragedStat<FillPoolResult>(runningAverageWindowSize); - const taskPollingEvents$: Observable<Pick< - TaskRunStat, - 'polling' - >> = taskPollingLifecycle.events.pipe( + const taskPollingEvents$: Observable< + Pick<TaskRunStat, 'polling'> + > = taskPollingLifecycle.events.pipe( filter( (taskEvent: TaskLifecycleEvent) => isTaskPollingCycleEvent(taskEvent) && isOk<FillPoolResult, unknown>(taskEvent.event) diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index d5a06c76aaacd1..1a8108e34078d0 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -118,10 +118,9 @@ export class TaskPollingLifecycle { } = config; // the task poller that polls for work on fixed intervals and on demand - const poller$: Observable<Result< - FillPoolResult, - PollingError<string> - >> = createObservableMonitor<Result<FillPoolResult, PollingError<string>>, Error>( + const poller$: Observable< + Result<FillPoolResult, PollingError<string>> + > = createObservableMonitor<Result<FillPoolResult, PollingError<string>>, Error>( () => createTaskPoller<string, FillPoolResult>({ logger, diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts index 96b9526a5174e9..430b1070ea6185 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -17,9 +17,7 @@ export const migrations: SavedObjectMigrationMap = { function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, ...doc -}: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>): SavedObjectUnsanitizedDoc< - TaskInstance -> { +}: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>): SavedObjectUnsanitizedDoc<TaskInstance> { return { ...doc, attributes: { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index cb44144d2fefe6..04ee3529bcc0be 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -186,9 +186,10 @@ export class TaskStore { * * @param opts - The query options used to filter tasks */ - public async fetch({ sort = [{ 'task.runAt': 'asc' }], ...opts }: SearchOpts = {}): Promise< - FetchResult - > { + public async fetch({ + sort = [{ 'task.runAt': 'asc' }], + ...opts + }: SearchOpts = {}): Promise<FetchResult> { return this.search({ ...opts, sort, @@ -380,9 +381,9 @@ export class TaskStore { let updatedSavedObjects: Array<SavedObjectsUpdateResponse | Error>; try { - ({ saved_objects: updatedSavedObjects } = await this.savedObjectsRepository.bulkUpdate< - SerializedConcreteTaskInstance - >( + ({ + saved_objects: updatedSavedObjects, + } = await this.savedObjectsRepository.bulkUpdate<SerializedConcreteTaskInstance>( docs.map((doc) => ({ type: 'task', id: doc.id, diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index 536c1d886e758b..73e8211b6fb818 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -69,9 +69,10 @@ export const usePivotData = ( aggs: PivotAggsConfigDict, groupBy: PivotGroupByConfigDict ): UseIndexDataReturnType => { - const [previewMappingsProperties, setPreviewMappingsProperties] = useState< - PreviewMappingsProperties - >({}); + const [ + previewMappingsProperties, + setPreviewMappingsProperties, + ] = useState<PreviewMappingsProperties>({}); const api = useApi(); const { ml: { formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, INDEX_STATUS }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index beb6325e4fecda..5bd43f4efd3e01 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": "緑", @@ -866,7 +865,6 @@ "data.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID", "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", - "data.indexPatterns.fetchFieldSaveErrorTitle": "インデックスパターンのフィールド取得後の保存中にエラーが発生 {title}(ID: {id})", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.noDataPopover.content": "この時間範囲にはデータが含まれていません表示するフィールドを増やし、グラフを作成するには、時間範囲を広げるか、調整してください。", "data.noDataPopover.dismissAction": "今後表示しない", @@ -2513,11 +2511,6 @@ "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", "indexPatternManagement.editIndexPattern.mappingConflictHeader": "マッピングの矛盾", "indexPatternManagement.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {フィールド} other {フィールド}}が、このパターンと一致するインデックスの間で異なるタイプ(文字列、整数など)に定義されています。これらの矛盾したフィールドはKibanaの一部で使用できますが、Kibanaがタイプを把握しなければならない機能には使用できません。この問題を修正するにはデータのレンダリングが必要です。", - "indexPatternManagement.editIndexPattern.refreshAria": "フィールドリストを再度読み込みます。", - "indexPatternManagement.editIndexPattern.refreshButton": "更新", - "indexPatternManagement.editIndexPattern.refreshHeader": "フィールドリストを更新しますか?", - "indexPatternManagement.editIndexPattern.refreshLabel": "この操作は各フィールドの使用頻度をリセットします。", - "indexPatternManagement.editIndexPattern.refreshTooltip": "フィールドリストを更新します。", "indexPatternManagement.editIndexPattern.removeAria": "インデックスパターンを削除します。", "indexPatternManagement.editIndexPattern.removeTooltip": "インデックスパターンを削除します。", "indexPatternManagement.editIndexPattern.scripted.addFieldButton": "スクリプトフィールドを追加", @@ -4961,7 +4954,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 +5071,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 +5176,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 +5276,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 +7164,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 +7198,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 +7543,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 +19448,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 +19779,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 +19821,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 +19829,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 +19986,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 d069d43de7404a..edf1704d90ac3f 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": "绿色", @@ -866,7 +865,6 @@ "data.functions.indexPatternLoad.id.help": "要加载的索引模式 id", "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", - "data.indexPatterns.fetchFieldSaveErrorTitle": "在提取索引模式 {title}(ID:{id})的字段后保存出错", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.noDataPopover.content": "此时间范围不包含任何数据。增大或调整时间范围,以查看更多的字段并创建图表。", "data.noDataPopover.dismissAction": "不再显示", @@ -2514,11 +2512,6 @@ "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "默认值", "indexPatternManagement.editIndexPattern.mappingConflictHeader": "映射冲突", "indexPatternManagement.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {一个字段} other {# 个字段}}已在匹配此模式的各个索引中定义为多个类型(字符串、整数等)。您也许仍能够在 Kibana 的各个部分中使用这些冲突类型,但它们将无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", - "indexPatternManagement.editIndexPattern.refreshAria": "重新加载字段列表。", - "indexPatternManagement.editIndexPattern.refreshButton": "刷新", - "indexPatternManagement.editIndexPattern.refreshHeader": "刷新字段列表?", - "indexPatternManagement.editIndexPattern.refreshLabel": "此操作重置每个字段的常见度计数器。", - "indexPatternManagement.editIndexPattern.refreshTooltip": "刷新字段列表。", "indexPatternManagement.editIndexPattern.removeAria": "移除索引模式。", "indexPatternManagement.editIndexPattern.removeTooltip": "移除索引模式。", "indexPatternManagement.editIndexPattern.scripted.addFieldButton": "添加脚本字段", @@ -4964,7 +4957,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 +5075,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 +5180,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 +5281,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 +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。", @@ -7220,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": "正在注册", @@ -7593,10 +7551,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 +19467,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 +19798,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 +19841,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 +19849,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 +20006,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/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index 111f6c9a47da92..a5f1f257120655 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -23,9 +23,9 @@ import { EuiLink } from '@elastic/eui'; import { ActionConnectorFieldsProps } from '../../../../types'; import { EmailActionConnector } from '../types'; -export const EmailActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps< - EmailActionConnector ->> = ({ action, editActionConfig, editActionSecrets, errors, readOnly, docLinks }) => { +export const EmailActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps<EmailActionConnector> +> = ({ action, editActionConfig, editActionSecrets, errors, readOnly, docLinks }) => { const { from, host, port, secure, hasAuth } = action.config; const { user, password } = action.secrets; useEffect(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index 9cfb9f1dc25b23..ba2f65659cd043 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -27,9 +27,9 @@ import { getIndexPatterns, } from '../../../../common/index_controls'; -const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps< - EsIndexActionConnector ->> = ({ action, editActionConfig, errors, http, readOnly, docLinks }) => { +const IndexActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps<EsIndexActionConnector> +> = ({ action, editActionConfig, errors, http, readOnly, docLinks }) => { const { index, refresh, executionTimeField } = action.config; const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState<boolean>( executionTimeField != null diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index ad2d5b3be52687..cc2e004d5a1d49 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; import { PagerDutyActionConnector } from '.././types'; -const PagerDutyActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps< - PagerDutyActionConnector ->> = ({ errors, action, editActionConfig, editActionSecrets, docLinks, readOnly }) => { +const PagerDutyActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps<PagerDutyActionConnector> +> = ({ errors, action, editActionConfig, editActionSecrets, docLinks, readOnly }) => { const { apiUrl } = action.config; const { routingKey } = action.secrets; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index c4f434f1387479..e8c427371c4a54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -11,9 +11,9 @@ import { ServerLogActionParams } from '.././types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { resolvedActionGroupMessage } from '../../../constants'; -export const ServerLogParamsFields: React.FunctionComponent<ActionParamsProps< - ServerLogActionParams ->> = ({ actionParams, editAction, index, errors, messageVariables, defaultMessage }) => { +export const ServerLogParamsFields: React.FunctionComponent< + ActionParamsProps<ServerLogActionParams> +> = ({ actionParams, editAction, index, errors, messageVariables, defaultMessage }) => { const { message, level } = actionParams; const levelOptions = [ { value: 'trace', text: 'Trace' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 66de29c68de2b1..06edb22f1c4c95 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -27,9 +27,9 @@ import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; import { connectorConfiguration } from './config'; -const ServiceNowConnectorFields: React.FC<ActionConnectorFieldsProps< - ServiceNowActionConnector ->> = ({ action, editActionSecrets, editActionConfig, errors, consumer, readOnly, docLinks }) => { +const ServiceNowConnectorFields: React.FC< + ActionConnectorFieldsProps<ServiceNowActionConnector> +> = ({ action, editActionSecrets, editActionConfig, errors, consumer, readOnly, docLinks }) => { // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution const { apiUrl, incidentConfiguration, isCaseOwned } = action.config; const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx index 3e59f2199153ba..ee4e34cd1ab8b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx @@ -23,9 +23,9 @@ import { TextAreaWithMessageVariables } from '../../text_area_with_message_varia import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { extractActionVariable } from '../extract_action_variable'; -const ServiceNowParamsFields: React.FunctionComponent<ActionParamsProps< - ServiceNowActionParams ->> = ({ actionParams, editAction, index, errors, messageVariables }) => { +const ServiceNowParamsFields: React.FunctionComponent< + ActionParamsProps<ServiceNowActionParams> +> = ({ actionParams, editAction, index, errors, messageVariables }) => { const { title, description, comment, severity, urgency, impact, savedObjectId } = actionParams.subActionParams || {}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index 0b37f340f01e24..d146e0c7a009d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; import { SlackActionConnector } from '../types'; -const SlackActionFields: React.FunctionComponent<ActionConnectorFieldsProps< - SlackActionConnector ->> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => { +const SlackActionFields: React.FunctionComponent< + ActionConnectorFieldsProps<SlackActionConnector> +> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => { const { webhookUrl } = action.secrets; return ( 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 index 41dfc1325e8ed6..7de0df33297965 100644 --- 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 @@ -10,9 +10,9 @@ 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 TeamsActionFields: React.FunctionComponent< + ActionConnectorFieldsProps<TeamsActionConnector> +> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => { const { webhookUrl } = action.secrets; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 1a9de48ccce3a3..8c54253c48c894 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -30,9 +30,9 @@ import { WebhookActionConnector } from '../types'; const HTTP_VERBS = ['post', 'put']; -const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps< - WebhookActionConnector ->> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { +const WebhookActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps<WebhookActionConnector> +> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { user, password } = action.secrets; const { method, url, headers, hasAuth } = action.config; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 3a1f9872a96a81..53121e5249abff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -15,6 +15,7 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, + EuiErrorBoundary, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -179,26 +180,28 @@ export const ActionConnectorForm = ({ </h4> </EuiTitle> <EuiSpacer size="s" /> - <Suspense - fallback={ - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="m" /> - </EuiFlexItem> - </EuiFlexGroup> - } - > - <FieldsComponent - action={connector} - errors={errors} - readOnly={!canSave} - editActionConfig={setActionConfigProperty} - editActionSecrets={setActionSecretsProperty} - http={http} - docLinks={docLinks} - consumer={consumer} - /> - </Suspense> + <EuiErrorBoundary> + <Suspense + fallback={ + <EuiFlexGroup justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </EuiFlexItem> + </EuiFlexGroup> + } + > + <FieldsComponent + action={connector} + errors={errors} + readOnly={!canSave} + editActionConfig={setActionConfigProperty} + editActionSecrets={setActionSecretsProperty} + http={http} + docLinks={docLinks} + consumer={consumer} + /> + </Suspense> + </EuiErrorBoundary> </> ) : null} </EuiForm> 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 5f1798d101d949..10c8498b181dca 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 @@ -24,6 +24,7 @@ import { EuiSuperSelect, EuiLoadingSpinner, EuiBadge, + EuiErrorBoundary, } from '@elastic/eui'; import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; import { @@ -254,28 +255,30 @@ export const ActionTypeForm = ({ </EuiFlexGroup> <EuiSpacer size="xl" /> {ParamsFieldsComponent ? ( - <Suspense - fallback={ - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="m" /> - </EuiFlexItem> - </EuiFlexGroup> - } - > - <ParamsFieldsComponent - actionParams={actionItem.params as any} - index={index} - errors={actionParamsErrors.errors} - editAction={setActionParamsProperty} - messageVariables={availableActionVariables} - defaultMessage={availableDefaultActionMessage} - docLinks={docLinks} - http={http} - toastNotifications={toastNotifications} - actionConnector={actionConnector} - /> - </Suspense> + <EuiErrorBoundary> + <Suspense + fallback={ + <EuiFlexGroup justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </EuiFlexItem> + </EuiFlexGroup> + } + > + <ParamsFieldsComponent + actionParams={actionItem.params as any} + index={index} + errors={actionParamsErrors.errors} + editAction={setActionParamsProperty} + messageVariables={availableActionVariables} + defaultMessage={availableDefaultActionMessage} + docLinks={docLinks} + http={http} + toastNotifications={toastNotifications} + actionConnector={actionConnector} + /> + </Suspense> + </EuiErrorBoundary> ) : null} </Fragment> ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx index 315254a003e896..4d9a327f97b054 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -14,6 +14,7 @@ import { EuiDescriptionList, EuiCallOut, EuiSpacer, + EuiErrorBoundary, } from '@elastic/eui'; import { Option, map, getOrElse } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -53,32 +54,34 @@ export const TestConnectorForm = ({ { title: 'Create an action', children: ParamsFieldsComponent ? ( - <Suspense - fallback={ - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="m" /> - </EuiFlexItem> - </EuiFlexGroup> - } - > - <ParamsFieldsComponent - actionParams={actionParams} - index={0} - errors={actionErrors.errors} - editAction={(field, value) => - setActionParams({ - ...actionParams, - [field]: value, - }) + <EuiErrorBoundary> + <Suspense + fallback={ + <EuiFlexGroup justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </EuiFlexItem> + </EuiFlexGroup> } - messageVariables={[]} - docLinks={docLinks} - http={http} - toastNotifications={toastNotifications} - actionConnector={connector} - /> - </Suspense> + > + <ParamsFieldsComponent + actionParams={actionParams} + index={0} + errors={actionErrors.errors} + editAction={(field, value) => + setActionParams({ + ...actionParams, + [field]: value, + }) + } + messageVariables={[]} + docLinks={docLinks} + http={http} + toastNotifications={toastNotifications} + actionConnector={connector} + /> + </Suspense> + </EuiErrorBoundary> ) : ( <EuiText> <p>This Connector does not require any Action Parameter.</p> 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 b06fb3c39ea459..7fd5bdc8d8707d 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 @@ -30,6 +30,7 @@ import { EuiLink, EuiText, EuiNotificationBadge, + EuiErrorBoundary, } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -461,19 +462,21 @@ export const AlertForm = ({ defaultActionGroupId && alert.alertTypeId && alertTypesIndex?.has(alert.alertTypeId) ? ( - <Suspense fallback={<CenterJustifiedSpinner />}> - <AlertParamsExpressionComponent - alertParams={alert.params} - alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`} - alertThrottle={`${alertThrottle ?? 1}${alertThrottleUnit}`} - errors={errors} - setAlertParams={setAlertParams} - setAlertProperty={setAlertProperty} - alertsContext={alertsContext} - defaultActionGroupId={defaultActionGroupId} - actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups} - /> - </Suspense> + <EuiErrorBoundary> + <Suspense fallback={<CenterJustifiedSpinner />}> + <AlertParamsExpressionComponent + alertParams={alert.params} + alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`} + alertThrottle={`${alertThrottle ?? 1}${alertThrottleUnit}`} + errors={errors} + setAlertParams={setAlertParams} + setAlertProperty={setAlertProperty} + alertsContext={alertsContext} + defaultActionGroupId={defaultActionGroupId} + actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups} + /> + </Suspense> + </EuiErrorBoundary> ) : null} {canShowActions && defaultActionGroupId && diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx index 06cd5809c951a3..2fbfccad74c97e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx @@ -12,10 +12,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { UpgradeAssistantTabProps } from './types'; -export const LoadingErrorBanner: React.FunctionComponent<Pick< - UpgradeAssistantTabProps, - 'loadingError' ->> = ({ loadingError }) => { +export const LoadingErrorBanner: React.FunctionComponent< + Pick<UpgradeAssistantTabProps, 'loadingError'> +> = ({ loadingError }) => { if (get(loadingError, 'response.status') === 403) { return ( <EuiCallOut diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 048dce6e4a5747..276e639678fd8e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -122,31 +122,31 @@ export function registerUpgradeAssistantUsageCollector({ usageCollection, savedObjects, }: Dependencies) { - const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector< - UpgradeAssistantTelemetry - >({ - type: 'upgrade-assistant-telemetry', - isReady: () => true, - schema: { - features: { - deprecation_logging: { - enabled: { type: 'boolean' }, + const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector<UpgradeAssistantTelemetry>( + { + type: 'upgrade-assistant-telemetry', + isReady: () => true, + schema: { + features: { + deprecation_logging: { + enabled: { type: 'boolean' }, + }, + }, + ui_open: { + cluster: { type: 'long' }, + indices: { type: 'long' }, + overview: { type: 'long' }, + }, + ui_reindex: { + close: { type: 'long' }, + open: { type: 'long' }, + start: { type: 'long' }, + stop: { type: 'long' }, }, }, - ui_open: { - cluster: { type: 'long' }, - indices: { type: 'long' }, - overview: { type: 'long' }, - }, - ui_reindex: { - close: { type: 'long' }, - open: { type: 'long' }, - start: { type: 'long' }, - stop: { type: 'long' }, - }, - }, - fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), - }); + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), + } + ); usageCollection.registerCollector(upgradeAssistantUsageCollector); } diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts index c622d4f19bade4..89eea46edb1122 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts @@ -7,17 +7,24 @@ import * as t from 'io-ts'; // IO type for validation -export const MonitorErrorType = t.partial({ - code: t.number, - message: t.string, - type: t.string, -}); +export const PingErrorType = t.intersection([ + t.partial({ + code: t.string, + id: t.string, + stack_trace: t.string, + type: t.string, + }), + t.type({ + // this is _always_ on the error field + message: t.string, + }), +]); // Typescript type for type checking -export type MonitorError = t.TypeOf<typeof MonitorErrorType>; +export type PingError = t.TypeOf<typeof PingErrorType>; export const MonitorDetailsType = t.intersection([ t.type({ monitorId: t.string }), - t.partial({ error: MonitorErrorType, timestamp: t.string, alerts: t.unknown }), + t.partial({ error: PingErrorType, timestamp: t.string, alerts: t.unknown }), ]); export type MonitorDetails = t.TypeOf<typeof MonitorDetailsType>; diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 315b8f543b800e..9e5cd7641b65d9 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -6,6 +6,7 @@ import * as t from 'io-ts'; import { DateRangeType } from '../common'; +import { PingErrorType } from '../monitor'; export const HttpResponseBodyType = t.partial({ bytes: t.number, @@ -116,18 +117,7 @@ export const PingType = t.intersection([ ecs: t.partial({ version: t.string, }), - error: t.intersection([ - t.partial({ - code: t.string, - id: t.string, - stack_trace: t.string, - type: t.string, - }), - t.type({ - // this is _always_ on the error field - message: t.string, - }), - ]), + error: PingErrorType, http: t.partial({ request: t.partial({ body: t.partial({ diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap index f6cc130b39fc1f..89433f8bc57c41 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap @@ -1823,7 +1823,9 @@ exports[`EmptyState component renders error message when an error occurs 1`] = ` <div className="euiText euiText--medium" > - <p> + <p + key="There was an error fetching your data." + > There was an error fetching your data. </p> </div> diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx index 165b123d8884db..f2d4de9f8be6ed 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx @@ -47,7 +47,9 @@ export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => { <Fragment> {!unauthorized && errors.map((error: IHttpFetchError) => ( - <p key={error.body.message}>{error.body.message || error.message}</p> + <p key={error.body.message || error.message}> + {error.body.message || error.message} + </p> ))} </Fragment> } diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx index e0b4c0dbd78f7d..8dd5ad48bc9d45 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import moment from 'moment'; import { BrowserRouter as Router } from 'react-router-dom'; import { MostRecentError } from '../most_recent_error'; -import { MonitorDetails, MonitorError } from '../../../../../../common/runtime_types'; +import { MonitorDetails, PingError } from '../../../../../../common/runtime_types'; describe('MostRecentError component', () => { let monitorDetails: MonitorDetails; - let monitorError: MonitorError; + let monitorError: PingError; beforeAll(() => { moment.prototype.fromNow = jest.fn(() => '5 days ago'); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx index 62f2f811aad98c..e7d9885680340f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx @@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n'; import { MonitorPageLink } from '../../../common/monitor_page_link'; import { useGetUrlParams } from '../../../../hooks'; import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; -import { MonitorError } from '../../../../../common/runtime_types'; +import { PingError } from '../../../../../common/runtime_types'; interface MostRecentErrorProps { /** * error returned from API for monitor details */ - error: MonitorError | undefined; + error: PingError | undefined; /** * monitorId to be used for link to detail page diff --git a/x-pack/plugins/uptime/public/state/actions/monitor.ts b/x-pack/plugins/uptime/public/state/actions/monitor.ts index a48b68db8126da..ff3f3cca33d096 100644 --- a/x-pack/plugins/uptime/public/state/actions/monitor.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor.ts @@ -6,7 +6,7 @@ import { createAction } from 'redux-actions'; import { MonitorDetailsActionPayload } from './types'; -import { MonitorError } from '../../../common/runtime_types'; +import { PingError } from '../../../common/runtime_types'; import { MonitorLocations } from '../../../common/runtime_types'; import { QueryParams } from './types'; import { createAsyncAction } from './utils'; @@ -17,7 +17,7 @@ export interface MonitorLocationsPayload extends QueryParams { export interface MonitorDetailsState { monitorId: string; - error: MonitorError; + error: PingError; } export const getMonitorDetailsAction = createAsyncAction< diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index cd98ba1600d34b..92965515f0876e 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -10,18 +10,16 @@ import { SavedObjectsClientContract, ISavedObjectsRepository, IScopedClusterClient, - ElasticsearchClient, } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; -import { DynamicSettings } from '../../../../common/runtime_types'; import { MlPluginSetup as MlSetup } from '../../../../../ml/server'; +import { UptimeESClient } from '../../lib'; export type UMElasticsearchQueryFn<P, R = any> = ( params: { - callES: ElasticsearchClient; + uptimeEsClient: UptimeESClient; esClient?: IScopedClusterClient; - dynamicSettings: DynamicSettings; } & P ) => Promise<R>; diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index 2126b484b1cfd3..3f6c3da2d6af06 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -14,6 +14,7 @@ import { import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PageViewParams, UptimeTelemetry, Usage } from './types'; import { savedObjectsAdapter } from '../../saved_objects'; +import { UptimeESClient } from '../../lib'; interface UptimeTelemetryCollector { [key: number]: UptimeTelemetry; @@ -131,7 +132,7 @@ export class KibanaTelemetryAdapter { } public static async countNoOfUniqueMonitorAndLocations( - callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] | ElasticsearchClient, + callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] | UptimeESClient, savedObjectsClient: ISavedObjectsRepository | SavedObjectsClientContract ) { const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 4f795e2aaf29e0..4f9fefa4188e5a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -96,13 +96,6 @@ describe('status check alert', () => { expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": undefined, "locations": Array [], "numTimes": 5, @@ -110,6 +103,12 @@ describe('status check alert', () => { "from": "now-15m", "to": "now", }, + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); @@ -152,13 +151,6 @@ describe('status check alert', () => { expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": undefined, "locations": Array [], "numTimes": 5, @@ -166,6 +158,12 @@ describe('status check alert', () => { "from": "now-15m", "to": "now", }, + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); @@ -333,13 +331,6 @@ describe('status check alert', () => { expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": Object { "bool": Object { "filter": Array [ @@ -506,6 +497,12 @@ describe('status check alert', () => { "from": "now-15m", "to": "now", }, + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); @@ -571,13 +568,6 @@ describe('status check alert', () => { expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": Object { "bool": Object { "filter": Array [ @@ -614,6 +604,12 @@ describe('status check alert', () => { "from": "now-30h", "to": "now", }, + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); @@ -758,17 +754,16 @@ describe('status check alert', () => { expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":12349}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":5601}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":443}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}]}}]}}", "range": 35, "rangeUnit": "d", "threshold": "99.34", + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); @@ -813,17 +808,16 @@ describe('status check alert', () => { expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}", "range": 23, "rangeUnit": "w", "threshold": "90", + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); @@ -857,17 +851,16 @@ describe('status check alert', () => { expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": [MockFunction], - "dynamicSettings": Object { - "certAgeThreshold": 730, - "certExpirationThreshold": 30, - "defaultConnectors": Array [], - "heartbeatIndices": "heartbeat-8*", - }, "filters": undefined, "range": 23, "rangeUnit": "w", "threshold": "90", + "uptimeEsClient": Object { + "baseESClient": [MockFunction], + "count": [Function], + "getSavedObjectsClient": [Function], + "search": [Function], + }, }, ] `); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index d4c26fe83b5fc1..022ec48bad1d93 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -82,7 +82,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _li context: [], state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], }, - async executor({ options, esClient, savedObjectsClient, dynamicSettings }) { + async executor({ options, uptimeEsClient, savedObjectsClient, dynamicSettings }) { const { services: { alertInstanceFactory }, state, @@ -96,8 +96,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _li if (foundAnomalies) { const monitorInfo = await getLatestMonitor({ - dynamicSettings, - callES: esClient, + uptimeEsClient, dateStart: 'now-15m', dateEnd: 'now', monitorId: params.monitorId, diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 577262c231977e..3e45ce302bf870 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -7,13 +7,11 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; -import { ElasticsearchClient } from 'kibana/server'; import { UptimeAlertTypeFactory } from './types'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; import { StatusCheckFilters, - DynamicSettings, Ping, GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; @@ -27,7 +25,7 @@ import { UNNAMED_LOCATION } from '../../../common/constants'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { MonitorStatusTranslations } from '../../../common/translations'; import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; -import { UMServerLibs } from '../lib'; +import { UMServerLibs, UptimeESClient } from '../lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -89,8 +87,7 @@ export const generateFilterDSL = async ( }; export const formatFilterString = async ( - dynamicSettings: DynamicSettings, - esClient: ElasticsearchClient, + uptimeEsClient: UptimeESClient, filters: StatusCheckFilters, search: string, libs?: UMServerLibs @@ -98,10 +95,9 @@ export const formatFilterString = async ( await generateFilterDSL( () => libs?.requests?.getIndexPattern - ? libs?.requests?.getIndexPattern({ esClient, dynamicSettings }) + ? libs?.requests?.getIndexPattern({ uptimeEsClient }) : getUptimeIndexPattern({ - esClient, - dynamicSettings, + uptimeEsClient, }), filters, search @@ -265,8 +261,8 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = state, services: { alertInstanceFactory }, }, - esClient, dynamicSettings, + uptimeEsClient, }) { const { filters, @@ -281,13 +277,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = timerange: oldVersionTimeRange, } = rawParams; - const filterString = await formatFilterString( - dynamicSettings, - esClient, - filters, - search, - libs - ); + const filterString = await formatFilterString(uptimeEsClient, filters, search, libs); const timerange = oldVersionTimeRange || { from: isAutoGenerated @@ -302,8 +292,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = // after that shouldCheckStatus should be explicitly false if (!(!oldVersionTimeRange && shouldCheckStatus === false)) { downMonitorsByLocation = await libs.requests.getMonitorStatus({ - callES: esClient, - dynamicSettings, + uptimeEsClient, timerange, numTimes, locations: [], @@ -337,8 +326,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = let availabilityResults: GetMonitorAvailabilityResult[] = []; if (shouldCheckAvailability) { availabilityResults = await libs.requests.getMonitorAvailability({ - callES: esClient, - dynamicSettings, + uptimeEsClient, ...availability, filters: JSON.stringify(filterString) || undefined, }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 11f602d10bf51c..41a5101716122d 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -100,15 +100,14 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => context: [], state: [...tlsTranslations.actionVariables, ...commonStateTranslations], }, - async executor({ options, dynamicSettings, esClient }) { + async executor({ options, dynamicSettings, uptimeEsClient }) { const { services: { alertInstanceFactory }, state, } = options; const { certs, total }: CertResult = await libs.requests.getCerts({ - callES: esClient, - dynamicSettings, + uptimeEsClient, from: DEFAULT_FROM, to: DEFAULT_TO, index: 0, diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts index 0961eb6557891e..965287ffbde8e3 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts @@ -4,19 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { AlertExecutorOptions, AlertType, AlertTypeState } from '../../../../alerts/server'; import { savedObjectsAdapter } from '../saved_objects'; import { DynamicSettings } from '../../../common/runtime_types'; +import { createUptimeESClient, UptimeESClient } from '../lib'; export interface UptimeAlertType extends Omit<AlertType, 'executor' | 'producer'> { executor: ({ options, - esClient, + uptimeEsClient, dynamicSettings, }: { options: AlertExecutorOptions; - esClient: ElasticsearchClient; + uptimeEsClient: UptimeESClient; dynamicSettings: DynamicSettings; savedObjectsClient: SavedObjectsClientContract; }) => Promise<AlertTypeState | void>; @@ -34,6 +35,8 @@ export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({ options.services.savedObjectsClient ); - return uptimeAlert.executor({ options, esClient, dynamicSettings, savedObjectsClient }); + const uptimeEsClient = createUptimeESClient({ esClient, savedObjectsClient }); + + return uptimeAlert.executor({ options, dynamicSettings, uptimeEsClient, savedObjectsClient }); }, }); diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index a7121eaec6679a..39dd868462525f 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -3,10 +3,12 @@ * 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, SavedObjectsClientContract } from 'kibana/server'; import { UMBackendFrameworkAdapter } from './adapters'; import { UMLicenseCheck } from './domains'; import { UptimeRequests } from './requests'; +import { savedObjectsAdapter } from './saved_objects'; +import { ESSearchResponse } from '../../../../typings/elasticsearch'; export interface UMDomainLibs { requests: UptimeRequests; @@ -16,3 +18,58 @@ export interface UMDomainLibs { export interface UMServerLibs extends UMDomainLibs { framework: UMBackendFrameworkAdapter; } + +export type UptimeESClient = ReturnType<typeof createUptimeESClient>; + +export function createUptimeESClient({ + esClient, + savedObjectsClient, +}: { + esClient: ElasticsearchClient; + savedObjectsClient: SavedObjectsClientContract; +}) { + return { + baseESClient: esClient, + async search<TParams>(params: TParams): Promise<{ body: ESSearchResponse<unknown, TParams> }> { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + savedObjectsClient! + ); + + let res: any; + try { + res = await esClient.search({ index: dynamicSettings!.heartbeatIndices, ...params }); + } catch (e) { + throw e; + } + return res; + }, + async count<TParams>( + params: TParams + ): Promise<{ + body: { + count: number; + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + }; + }> { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + savedObjectsClient! + ); + + let res: any; + try { + res = await esClient.count({ index: dynamicSettings!.heartbeatIndices, ...params }); + } catch (e) { + throw e; + } + return res; + }, + getSavedObjectsClient() { + return savedObjectsClient; + }, + }; +} diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap deleted file mode 100644 index 2f6d6e06f93e12..00000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractFilterAggsResults extracts the bucket values of the expected filter fields 1`] = ` -Object { - "locations": Array [ - "us-east-2", - "fairbanks", - ], - "ports": Array [ - 12349, - 80, - 5601, - 8200, - 9200, - 9292, - ], - "schemes": Array [ - "http", - "tcp", - "icmp", - ], - "tags": Array [ - "api", - "dev", - ], -} -`; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts deleted file mode 100644 index 19fd0fda8d83e7..00000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.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 { extractFilterAggsResults } from '../get_filter_bar'; - -describe('extractFilterAggsResults', () => { - it('extracts the bucket values of the expected filter fields', () => { - expect( - extractFilterAggsResults( - { - locations: { - doc_count: 8098, - term: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'us-east-2', doc_count: 4050 }, - { key: 'fairbanks', doc_count: 4048 }, - ], - }, - }, - schemes: { - doc_count: 8098, - term: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'http', doc_count: 5055 }, - { key: 'tcp', doc_count: 2685 }, - { key: 'icmp', doc_count: 358 }, - ], - }, - }, - ports: { - doc_count: 8098, - term: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 12349, doc_count: 3571 }, - { key: 80, doc_count: 2985 }, - { key: 5601, doc_count: 358 }, - { key: 8200, doc_count: 358 }, - { key: 9200, doc_count: 358 }, - { key: 9292, doc_count: 110 }, - ], - }, - }, - tags: { - doc_count: 8098, - term: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'api', doc_count: 8098 }, - { key: 'dev', doc_count: 8098 }, - ], - }, - }, - }, - ['locations', 'ports', 'schemes', 'tags'] - ) - ).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index c0b94b19b75825..cb37438619397e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -5,8 +5,7 @@ */ import { getCerts } from '../get_certs'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import { getUptimeESMockClient } from './helper'; describe('getCerts', () => { let mockHits: any; @@ -82,8 +81,9 @@ describe('getCerts', () => { }); it('parses query result and returns expected values', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); - mockEsClient.search.mockResolvedValueOnce({ + const { esClient, uptimeEsClient } = getUptimeESMockClient(); + + esClient.search.mockResolvedValueOnce({ body: { hits: { hits: mockHits, @@ -92,13 +92,7 @@ describe('getCerts', () => { } as any); const result = await getCerts({ - callES: mockEsClient, - dynamicSettings: { - heartbeatIndices: 'heartbeat*', - certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, - certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, - defaultConnectors: [], - }, + uptimeEsClient, index: 1, from: 'now-2d', to: 'now+1h', @@ -129,7 +123,7 @@ describe('getCerts', () => { "total": 0, } `); - expect(mockEsClient.search.mock.calls).toMatchInlineSnapshot(` + expect(esClient.search.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -217,7 +211,7 @@ describe('getCerts', () => { }, ], }, - "index": "heartbeat*", + "index": "heartbeat-8*", }, ], ] diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index 9503174ed104c1..3e3a6878b18b92 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -6,7 +6,7 @@ import { getLatestMonitor } from '../get_latest_monitor'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import { getUptimeESMockClient } from './helper'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; @@ -69,12 +69,12 @@ describe('getLatestMonitor', () => { }); it('returns data in expected shape', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); const result = await getLatestMonitor({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateStart: 'now-1h', dateEnd: 'now', monitorId: 'testMonitor', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts index e8df65d4101679..82256f31067c39 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts @@ -10,9 +10,9 @@ import { AvailabilityKey, getMonitorAvailability, } from '../get_monitor_availability'; -import { setupMockEsCompositeQuery } from './helper'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; +import { getUptimeESMockClient, setupMockEsCompositeQuery } from './helper'; import { GetMonitorAvailabilityParams, makePing, Ping } from '../../../../common/runtime_types'; + interface AvailabilityTopHit { _source: Ping; } @@ -108,9 +108,11 @@ describe('monitor availability', () => { "minimum_should_match": 1 } }`; + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + await getMonitorAvailability({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, filters: exampleFilter, range: 2, rangeUnit: 'w', @@ -286,9 +288,11 @@ describe('monitor availability', () => { rangeUnit: 'd', threshold: '69', }; + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + const result = await getMonitorAvailability({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, ...clientParameters, }); expect(esMock.search).toHaveBeenCalledTimes(1); @@ -509,9 +513,10 @@ describe('monitor availability', () => { ], genBucketItem ); + const { uptimeEsClient } = getUptimeESMockClient(esMock); + const result = await getMonitorAvailability({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, range: 3, rangeUnit: 'M', threshold: '98', @@ -812,9 +817,11 @@ describe('monitor availability', () => { ], genBucketItem ); + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + await getMonitorAvailability({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, range: 3, rangeUnit: 's', threshold: '99', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index 9edd3e2e160d24..428f990352dfca 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -7,16 +7,16 @@ import { set } from '@elastic/safer-lodash-set'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import { getUptimeESMockClient } from './helper'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will provide expected filters', async () => { expect.assertions(2); - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + await getMonitorDurationChart({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, monitorId: 'fooID', dateStart: 'now-15m', dateEnd: 'now', @@ -33,13 +33,13 @@ describe('ElasticsearchMonitorsAdapter', () => { }); it('inserts empty buckets for missing data', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockChartsData as any); expect( await getMonitorDurationChart({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, monitorId: 'id', dateStart: 'now-15m', dateEnd: 'now', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index 949bc39f072593..4978fbd6bcdbb5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -5,8 +5,7 @@ */ import { getMonitorStatus } from '../get_monitor_status'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { setupMockEsCompositeQuery } from './helper'; +import { getUptimeESMockClient, setupMockEsCompositeQuery } from './helper'; export interface BucketItemCriteria { monitorId: string; @@ -77,9 +76,11 @@ describe('getMonitorStatus', () => { minimum_should_match: 1, }, }; + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + await getMonitorStatus({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, filters: exampleFilter, locations: [], numTimes: 5, @@ -193,9 +194,11 @@ describe('getMonitorStatus', () => { [], genBucketItem ); + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + await getMonitorStatus({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, locations: ['fairbanks', 'harrisburg'], numTimes: 1, timerange: { @@ -350,9 +353,11 @@ describe('getMonitorStatus', () => { }, }, }; + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + await getMonitorStatus({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, ...clientParameters, }); expect(esMock.search).toHaveBeenCalledTimes(1); @@ -495,9 +500,11 @@ describe('getMonitorStatus', () => { }, }, }; + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + await getMonitorStatus({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, ...clientParameters, }); expect(esMock.search).toHaveBeenCalledTimes(1); @@ -615,9 +622,11 @@ describe('getMonitorStatus', () => { to: 'now-2m', }, }; + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + const result = await getMonitorStatus({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, ...clientParameters, }); expect(esMock.search).toHaveBeenCalledTimes(1); @@ -793,9 +802,11 @@ describe('getMonitorStatus', () => { criteria, genBucketItem ); + + const { uptimeEsClient } = getUptimeESMockClient(esMock); + const result = await getMonitorStatus({ - callES: esMock, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, locations: [], numTimes: 5, timerange: { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 427061b6c16d43..3b26adb18ae7f0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -5,9 +5,8 @@ */ import { getPingHistogram } from '../get_ping_histogram'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import * as intervalHelper from '../../helper/get_histogram_interval'; +import { getUptimeESMockClient } from './helper'; describe('getPingHistogram', () => { beforeEach(() => { @@ -43,7 +42,7 @@ describe('getPingHistogram', () => { it('returns a single bucket if array has 1', async () => { expect.assertions(2); - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); mockEsClient.search.mockResolvedValueOnce({ body: { @@ -67,8 +66,7 @@ describe('getPingHistogram', () => { } as any); const result = await getPingHistogram({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, from: 'now-15m', to: 'now', }); @@ -80,7 +78,7 @@ describe('getPingHistogram', () => { it('returns expected result for no status filter', async () => { expect.assertions(2); - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); standardMockResponse.aggregations.timeseries.interval = '1m'; @@ -89,8 +87,7 @@ describe('getPingHistogram', () => { } as any); const result = await getPingHistogram({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, from: 'now-15m', to: 'now', filters: '', @@ -103,7 +100,7 @@ describe('getPingHistogram', () => { it('handles status + additional user queries', async () => { expect.assertions(2); - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); mockEsClient.search.mockResolvedValueOnce({ body: { @@ -154,8 +151,7 @@ describe('getPingHistogram', () => { }; const result = await getPingHistogram({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, from: 'now-15m', to: 'now', filters: JSON.stringify(searchFilter), @@ -168,7 +164,7 @@ describe('getPingHistogram', () => { it('handles simple_text_query without issues', async () => { expect.assertions(2); - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); mockEsClient.search.mockResolvedValueOnce({ body: { @@ -211,8 +207,7 @@ describe('getPingHistogram', () => { const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; const result = await getPingHistogram({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, from: 'now-15m', to: 'now', filters, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index f313cce9f758bb..9b28d58c7e8c29 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -7,7 +7,7 @@ import { getPings } from '../get_pings'; import { set } from '@elastic/safer-lodash-set'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import { getUptimeESMockClient } from './helper'; describe('getAll', () => { let mockEsSearchResult: any; @@ -87,12 +87,12 @@ describe('getAll', () => { }); it('returns data in the appropriate shape', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); + const result = await getPings({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -110,11 +110,12 @@ describe('getAll', () => { }); it('creates appropriate sort and size parameters', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); + await getPings({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -126,7 +127,7 @@ describe('getAll', () => { Array [ Object { "body": Object { - "aggregations": Object { + "aggs": Object { "locations": Object { "terms": Object { "field": "observer.geo.name", @@ -189,11 +190,12 @@ describe('getAll', () => { }); it('omits the sort param when no sort passed', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); + await getPings({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateRange: { from: 'now-1h', to: 'now' }, size: 12, }); @@ -203,7 +205,7 @@ describe('getAll', () => { Array [ Object { "body": Object { - "aggregations": Object { + "aggs": Object { "locations": Object { "terms": Object { "field": "observer.geo.name", @@ -266,11 +268,12 @@ describe('getAll', () => { }); it('omits the size param when no size passed', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); + await getPings({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateRange: { from: 'now-1h', to: 'now' }, sort: 'desc', }); @@ -280,7 +283,7 @@ describe('getAll', () => { Array [ Object { "body": Object { - "aggregations": Object { + "aggs": Object { "locations": Object { "terms": Object { "field": "observer.geo.name", @@ -343,11 +346,12 @@ describe('getAll', () => { }); it('adds a filter for monitor ID', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); + await getPings({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateRange: { from: 'now-1h', to: 'now' }, monitorId: 'testmonitorid', }); @@ -357,7 +361,7 @@ describe('getAll', () => { Array [ Object { "body": Object { - "aggregations": Object { + "aggs": Object { "locations": Object { "terms": Object { "field": "observer.geo.name", @@ -425,11 +429,12 @@ describe('getAll', () => { }); it('adds a filter for monitor status', async () => { - const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult); + await getPings({ - callES: mockEsClient, - dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + uptimeEsClient, dateRange: { from: 'now-1h', to: 'now' }, status: 'down', }); @@ -439,7 +444,7 @@ describe('getAll', () => { Array [ Object { "body": Object { - "aggregations": Object { + "aggs": Object { "locations": Object { "terms": Object { "field": "observer.geo.name", diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts index 4ebc9b2da78558..37f75833128677 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; +import { createUptimeESClient } from '../../lib'; export interface MultiPageCriteria<K, T> { after_key?: K; @@ -54,3 +58,17 @@ export const setupMockEsCompositeQuery = <K, C, I>( return esMock; }; + +export const getUptimeESMockClient = (esClientMock?: ElasticsearchClientMock) => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + + const savedObjectsClient = savedObjectsClientMock.create(); + + return { + esClient: esClientMock || esClient, + uptimeEsClient: createUptimeESClient({ + esClient: esClientMock || esClient, + savedObjectsClient, + }), + }; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index 0836cb039b215e..d8b8f3733b94b3 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { CertResult, GetCertsParams } from '../../../common/runtime_types'; +import { CertResult, GetCertsParams, Ping } from '../../../common/runtime_types'; enum SortFields { 'issuer' = 'tls.server.x509.issuer.common_name', @@ -15,8 +15,7 @@ enum SortFields { } export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = async ({ - callES, - dynamicSettings, + uptimeEsClient, index, from, to, @@ -29,92 +28,86 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn }) => { const sort = SortFields[sortBy as keyof typeof SortFields]; - const params: any = { - index: dynamicSettings.heartbeatIndices, - body: { - from: index * size, - size, - sort: [ - { - [sort]: { - order: direction, - }, + const searchBody = { + from: index * size, + size, + sort: [ + { + [sort]: { + order: direction as 'asc' | 'desc', }, - ], - query: { - bool: { - filter: [ - { - exists: { - field: 'tls.server', - }, - }, - { - range: { - 'monitor.timespan': { - gte: from, - lte: to, + }, + ], + query: { + bool: { + ...(search + ? { + minimum_should_match: 1, + should: [ + { + multi_match: { + query: escape(search), + type: 'phrase_prefix', + fields: [ + 'monitor.id.text', + 'monitor.name.text', + 'url.full.text', + 'tls.server.x509.subject.common_name.text', + 'tls.server.x509.issuer.common_name.text', + ], + }, }, - }, + ], + } + : {}), + filter: [ + { + exists: { + field: 'tls.server', }, - ], - }, - }, - _source: [ - 'monitor.id', - 'monitor.name', - 'tls.server.x509.issuer.common_name', - 'tls.server.x509.subject.common_name', - 'tls.server.hash.sha1', - 'tls.server.hash.sha256', - 'tls.server.x509.not_after', - 'tls.server.x509.not_before', - ], - collapse: { - field: 'tls.server.hash.sha256', - inner_hits: { - _source: { - includes: ['monitor.id', 'monitor.name', 'url.full'], }, - collapse: { - field: 'monitor.id', + { + range: { + 'monitor.timespan': { + gte: from, + lte: to, + }, + }, }, - name: 'monitors', - sort: [{ 'monitor.id': 'asc' }], - }, + ], }, - aggs: { - total: { - cardinality: { - field: 'tls.server.hash.sha256', - }, + }, + _source: [ + 'monitor.id', + 'monitor.name', + 'tls.server.x509.issuer.common_name', + 'tls.server.x509.subject.common_name', + 'tls.server.hash.sha1', + 'tls.server.hash.sha256', + 'tls.server.x509.not_after', + 'tls.server.x509.not_before', + ], + collapse: { + field: 'tls.server.hash.sha256', + inner_hits: { + _source: { + includes: ['monitor.id', 'monitor.name', 'url.full'], + }, + collapse: { + field: 'monitor.id', }, + name: 'monitors', + sort: [{ 'monitor.id': 'asc' }], }, }, - }; - - if (!params.body.query.bool.should) { - params.body.query.bool.should = []; - } - - if (search) { - params.body.query.bool.minimum_should_match = 1; - params.body.query.bool.should = [ - { - multi_match: { - query: escape(search), - type: 'phrase_prefix', - fields: [ - 'monitor.id.text', - 'monitor.name.text', - 'url.full.text', - 'tls.server.x509.subject.common_name.text', - 'tls.server.x509.issuer.common_name.text', - ], + aggs: { + total: { + cardinality: { + field: 'tls.server.hash.sha256', }, }, - ]; - } + }, + }; if (notValidBefore || notValidAfter) { const validityFilters: any = { @@ -141,18 +134,17 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn }); } - params.body.query.bool.filter.push(validityFilters); + searchBody.query.bool.filter.push(validityFilters); } // console.log(JSON.stringify(params, null, 2)); - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ + body: searchBody, + }); - const certs = (result?.hits?.hits ?? []).map((hit: any) => { - const { - _source: { - tls: { server }, - }, - } = hit; + const certs = (result?.hits?.hits ?? []).map((hit) => { + const ping = hit._source as Ping; + const server = ping.tls?.server!; const notAfter = server?.x509?.not_after; const notBefore = server?.x509?.not_before; @@ -171,7 +163,7 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn monitors, issuer, sha1, - sha256, + sha256: sha256 as string, not_after: notAfter, not_before: notBefore, common_name: commonName, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts index c3295d6dd9c8f7..026ce184649cde 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -45,28 +45,8 @@ export const combineRangeWithFilters = ( return filters; }; -type SupportedFields = 'locations' | 'ports' | 'schemes' | 'tags'; - -export const extractFilterAggsResults = ( - responseAggregations: Record<string, any>, - keys: SupportedFields[] -): OverviewFilters => { - const values: OverviewFilters = { - locations: [], - ports: [], - schemes: [], - tags: [], - }; - keys.forEach((key) => { - const buckets = responseAggregations?.[key]?.term?.buckets ?? []; - values[key] = buckets.map((item: { key: string | number }) => item.key); - }); - return values; -}; - export const getFilterBar: UMElasticsearchQueryFn<GetFilterBarParams, OverviewFilters> = async ({ - callES, - dynamicSettings, + uptimeEsClient, dateRangeStart, dateRangeEnd, search, @@ -82,19 +62,24 @@ export const getFilterBar: UMElasticsearchQueryFn<GetFilterBarParams, OverviewFi filterOptions ); const filters = combineRangeWithFilters(dateRangeStart, dateRangeEnd, search); - const params = { - index: dynamicSettings.heartbeatIndices, - body: { - size: 0, - query: { - ...filters, - }, - aggs, + const searchBody = { + size: 0, + query: { + ...filters, }, + aggs, }; const { body: { aggregations }, - } = await callES.search(params); - return extractFilterAggsResults(aggregations, ['tags', 'locations', 'ports', 'schemes']); + } = await uptimeEsClient.search({ body: searchBody }); + + const { tags, locations, ports, schemes } = aggregations ?? {}; + + return { + locations: locations?.term?.buckets.map((item) => item.key as string), + ports: ports?.term?.buckets.map((item) => item.key as number), + schemes: schemes?.term?.buckets.map((item) => item.key as string), + tags: tags?.term?.buckets.map((item) => item.key as string), + }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 98d32b16b28846..ab0b9043d14e27 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor, IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; -import { DynamicSettings } from '../../../common/runtime_types'; +import { UptimeESClient } from '../lib'; +import { savedObjectsAdapter } from '../saved_objects'; export interface IndexPatternTitleAndFields { title: string; @@ -14,14 +14,15 @@ export interface IndexPatternTitleAndFields { } export const getUptimeIndexPattern = async ({ - esClient, - dynamicSettings, + uptimeEsClient, }: { - esClient: ElasticsearchClient; - dynamicSettings: DynamicSettings; + uptimeEsClient: UptimeESClient; }): Promise<IndexPatternTitleAndFields | undefined> => { - const indexPatternsFetcher = new IndexPatternsFetcher(esClient); + const indexPatternsFetcher = new IndexPatternsFetcher(uptimeEsClient.baseESClient); + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + uptimeEsClient.getSavedObjectsClient()! + ); // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) // and since `getFieldsForWildcard` will throw if the specified indices don't exist, // we have to catch errors here to avoid all endpoints returning 500 for users without APM data diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index 061d002b010de4..e2baf39905bfdc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -8,15 +8,14 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { StatesIndexStatus } from '../../../common/runtime_types'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ - callES, - dynamicSettings, + uptimeEsClient, }) => { const { body: { _shards: { total }, count, }, - } = await callES.count({ index: dynamicSettings.heartbeatIndices }); + } = await uptimeEsClient.count({}); return { indexExists: total > 0, docCount: count, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index bff3aaf1176df3..dacdcaff7dfd51 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -5,6 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters/framework'; +import { ESSearchBody } from '../../../../../typings/elasticsearch'; interface GetJourneyScreenshotParams { checkGroup: string; @@ -14,35 +15,32 @@ interface GetJourneyScreenshotParams { export const getJourneyScreenshot: UMElasticsearchQueryFn< GetJourneyScreenshotParams, any -> = async ({ callES, dynamicSettings, checkGroup, stepIndex }) => { - const params: any = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ - { - term: { - 'monitor.check_group': checkGroup, - }, +> = async ({ uptimeEsClient, checkGroup, stepIndex }) => { + const params: ESSearchBody = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.check_group': checkGroup, }, - { - term: { - 'synthetics.type': 'step/screenshot', - }, + }, + { + term: { + 'synthetics.type': 'step/screenshot', }, - { - term: { - 'synthetics.step.index': stepIndex, - }, + }, + { + term: { + 'synthetics.step.index': stepIndex, }, - ], - }, + }, + ], }, - _source: ['synthetics.blob'], }, + _source: ['synthetics.blob'], }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); if (!Array.isArray(result?.hits?.hits) || result.hits.hits.length < 1) { return null; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index f36815a747db3d..c330e1b66fe93c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -12,51 +12,50 @@ interface GetJourneyStepsParams { } export const getJourneySteps: UMElasticsearchQueryFn<GetJourneyStepsParams, Ping> = async ({ - callES, - dynamicSettings, + uptimeEsClient, checkGroup, }) => { - const params: any = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ - { - terms: { - 'synthetics.type': ['step/end', 'stderr', 'cmd/status', 'step/screenshot'], - }, + const params = { + query: { + bool: { + filter: [ + { + terms: { + 'synthetics.type': ['step/end', 'stderr', 'cmd/status', 'step/screenshot'], }, - { - term: { - 'monitor.check_group': checkGroup, - }, + }, + { + term: { + 'monitor.check_group': checkGroup, }, - ], - }, - }, - sort: [{ 'synthetics.step.index': { order: 'asc' } }, { '@timestamp': { order: 'asc' } }], - _source: { - excludes: ['synthetics.blob'], + }, + ], }, }, + sort: [{ 'synthetics.step.index': { order: 'asc' } }, { '@timestamp': { order: 'asc' } }], + _source: { + excludes: ['synthetics.blob'], + }, size: 500, }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); + const screenshotIndexes: number[] = result.hits.hits - .filter((h: any) => h?._source?.synthetics?.type === 'step/screenshot') - .map((h: any) => h?._source?.synthetics?.step?.index); - return result.hits.hits - .filter((h: any) => h?._source?.synthetics?.type !== 'step/screenshot') - .map( - ({ _id, _source, _source: { synthetics } }: any): Ping => ({ - ..._source, - timestamp: _source['@timestamp'], - docId: _id, + .filter((h) => (h?._source as Ping).synthetics?.type === 'step/screenshot') + .map((h) => (h?._source as Ping).synthetics?.step?.index as number); + + return (result.hits.hits + .filter((h) => (h?._source as Ping).synthetics?.type !== 'step/screenshot') + .map((h) => { + const source = h._source as Ping & { '@timestamp': string }; + return { + ...source, + timestamp: source['@timestamp'], + docId: h._id, synthetics: { - ...synthetics, - screenshotExists: screenshotIndexes.some((i) => i === synthetics?.step?.index), + ...source.synthetics, + screenshotExists: screenshotIndexes.some((i) => i === source.synthetics?.step?.index), }, - }) - ); + }; + }) as unknown) as Ping; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index f6562eaa42e900..1e323b57b30dc0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -22,45 +22,42 @@ export interface GetLatestMonitorParams { // Get The monitor latest state sorted by timestamp with date range export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Ping> = async ({ - callES, - dynamicSettings, + uptimeEsClient, dateStart, dateEnd, monitorId, observerLocation, }) => { const params = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ - { exists: { field: 'summary' } }, - { - range: { - '@timestamp': { - gte: dateStart, - lte: dateEnd, - }, + query: { + bool: { + filter: [ + { exists: { field: 'summary' } }, + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, }, }, - ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), - ...(observerLocation ? [{ term: { 'observer.geo.name': observerLocation } }] : []), - ], - }, - }, - size: 1, - _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http', 'error'], - sort: { - '@timestamp': { order: 'desc' }, + }, + ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), + ...(observerLocation ? [{ term: { 'observer.geo.name': observerLocation } }] : []), + ], }, }, + size: 1, + _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http', 'error'], + sort: { + '@timestamp': { order: 'desc' }, + }, }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); + const doc = result.hits?.hits?.[0]; const docId = doc?._id ?? ''; - const { tls, ...ping } = doc?._source ?? {}; + const { tls, ...ping } = (doc?._source as Ping & { '@timestamp': string }) ?? {}; return { ...ping, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts index 2f1a37095c3bca..04b4eef19d689b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts @@ -6,6 +6,8 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { GetMonitorAvailabilityParams, Ping } from '../../../common/runtime_types'; +import { AfterKey } from './get_monitor_status'; +import { SortOptions } from '../../../../../typings/elasticsearch'; export interface AvailabilityKey { monitorId: string; @@ -34,9 +36,9 @@ export const formatBuckets = async (buckets: any[]): Promise<GetMonitorAvailabil export const getMonitorAvailability: UMElasticsearchQueryFn< GetMonitorAvailabilityParams, GetMonitorAvailabilityResult[] -> = async ({ callES, dynamicSettings, range, rangeUnit, threshold: thresholdString, filters }) => { +> = async ({ uptimeEsClient, range, rangeUnit, threshold: thresholdString, filters }) => { const queryResults: Array<Promise<GetMonitorAvailabilityResult[]>> = []; - let afterKey: AvailabilityKey | undefined; + let afterKey: AfterKey; const threshold = Number(thresholdString) / 100; if (threshold <= 0 || threshold > 1.0) { @@ -53,92 +55,90 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< } do { - const esParams: any = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ + const esParams = { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte, + lte: 'now', + }, + }, + }, + // append user filters, if defined + ...(parsedFilters?.bool ? [parsedFilters] : []), + ], + }, + }, + size: 0, + aggs: { + monitors: { + composite: { + size: 2000, + ...(afterKey ? { after: afterKey } : {}), + sources: [ { - range: { - '@timestamp': { - gte, - lte: 'now', + monitorId: { + terms: { + field: 'monitor.id', }, }, }, - // append user filters, if defined - ...(parsedFilters?.bool ? [parsedFilters] : []), - ], - }, - }, - size: 0, - aggs: { - monitors: { - composite: { - size: 2000, - sources: [ - { - monitorId: { - terms: { - field: 'monitor.id', - }, + { + location: { + terms: { + field: 'observer.geo.name', + missing_bucket: true, }, }, - { - location: { - terms: { - field: 'observer.geo.name', - missing_bucket: true, + }, + ], + }, + aggs: { + fields: { + top_hits: { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc', }, }, - }, - ], + ] as SortOptions, + }, }, - aggs: { - fields: { - top_hits: { - size: 1, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, + up_sum: { + sum: { + field: 'summary.up', + missing: 0, }, - up_sum: { - sum: { - field: 'summary.up', - missing: 0, - }, + }, + down_sum: { + sum: { + field: 'summary.down', + missing: 0, }, - down_sum: { - sum: { - field: 'summary.down', - missing: 0, + }, + ratio: { + bucket_script: { + buckets_path: { + upTotal: 'up_sum', + downTotal: 'down_sum', }, - }, - ratio: { - bucket_script: { - buckets_path: { - upTotal: 'up_sum', - downTotal: 'down_sum', - }, - script: ` + script: ` if (params.upTotal + params.downTotal > 0) { return params.upTotal / (params.upTotal + params.downTotal); } return null;`, - }, }, - filtered: { - bucket_selector: { - buckets_path: { - threshold: 'ratio.value', - }, - script: `params.threshold < ${threshold}`, + }, + filtered: { + bucket_selector: { + buckets_path: { + threshold: 'ratio.value', }, + script: `params.threshold < ${threshold}`, }, }, }, @@ -146,12 +146,9 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< }, }; - if (afterKey) { - esParams.body.aggs.monitors.composite.after = afterKey; - } + const { body: result } = await uptimeEsClient.search({ body: esParams }); - const { body: result } = await callES.search(esParams); - afterKey = result?.aggregations?.monitors?.after_key; + afterKey = result?.aggregations?.monitors?.after_key as AfterKey; queryResults.push(formatBuckets(result?.aggregations?.monitors?.buckets || [])); } while (afterKey !== undefined); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index 998ea3dd9df000..c4d122515d1336 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchClient } from 'kibana/server'; import { UMElasticsearchQueryFn } from '../adapters'; -import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; +import { MonitorDetails, Ping } from '../../../common/runtime_types'; import { formatFilterString } from '../alerts/status_check'; +import { UptimeESClient } from '../lib'; +import { ESSearchBody } from '../../../../../typings/elasticsearch'; export interface GetMonitorDetailsParams { monitorId: string; @@ -17,13 +18,11 @@ export interface GetMonitorDetailsParams { } const getMonitorAlerts = async ({ - callES, - dynamicSettings, + uptimeEsClient, alertsClient, monitorId, }: { - callES: ElasticsearchClient; - dynamicSettings: any; + uptimeEsClient: UptimeESClient; alertsClient: any; monitorId: string; }) => { @@ -44,41 +43,37 @@ const getMonitorAlerts = async ({ monitorAlerts.push(currAlert); continue; } - const esParams: any = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ - { - term: { - 'monitor.id': monitorId, - }, + const esParams: ESSearchBody = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.id': monitorId, }, - ], - }, - }, - size: 0, - aggs: { - monitors: { - terms: { - field: 'monitor.id', - size: 1000, }, + ], + }, + }, + size: 0, + aggs: { + monitors: { + terms: { + field: 'monitor.id', + size: 1000, }, }, }, }; const parsedFilters = await formatFilterString( - dynamicSettings, - callES, + uptimeEsClient, currAlert.params.filters, currAlert.params.search ); - esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, parsedFilters?.bool); + esParams.query.bool = Object.assign({}, esParams.query.bool, parsedFilters?.bool); - const { body: result } = await callES.search(esParams); + const { body: result } = await uptimeEsClient.search({ body: esParams }); if (result.hits.total.value > 0) { monitorAlerts.push(currAlert); @@ -90,7 +85,7 @@ const getMonitorAlerts = async ({ export const getMonitorDetails: UMElasticsearchQueryFn< GetMonitorDetailsParams, MonitorDetails -> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => { +> = async ({ uptimeEsClient, monitorId, dateStart, dateEnd, alertsClient }) => { const queryFilters: any = [ { range: { @@ -108,48 +103,43 @@ export const getMonitorDetails: UMElasticsearchQueryFn< ]; const params = { - index: dynamicSettings.heartbeatIndices, - body: { - size: 1, - _source: ['error', '@timestamp'], - query: { - bool: { - must: [ - { - exists: { - field: 'error', - }, + size: 1, + _source: ['error', '@timestamp'], + query: { + bool: { + must: [ + { + exists: { + field: 'error', }, - ], - filter: queryFilters, - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', }, - }, - ], + ], + filter: queryFilters, + }, }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); - const data = result.hits.hits[0]?._source; + const data = result.hits.hits[0]?._source as Ping & { '@timestamp': string }; - const monitorError: MonitorError | undefined = data?.error; const errorTimestamp: string | undefined = data?.['@timestamp']; const monAlerts = await getMonitorAlerts({ - callES, - dynamicSettings, + uptimeEsClient, alertsClient, monitorId, }); return { monitorId, - error: monitorError, + error: data?.error, timestamp: errorTimestamp, alerts: monAlerts, }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts index 77ae7570a96a84..cb1251eb7f9db5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts @@ -23,35 +23,32 @@ export interface GetMonitorChartsParams { export const getMonitorDurationChart: UMElasticsearchQueryFn< GetMonitorChartsParams, MonitorDurationResult -> = async ({ callES, dynamicSettings, dateStart, dateEnd, monitorId }) => { +> = async ({ uptimeEsClient, dateStart, dateEnd, monitorId }) => { const params = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: dateStart, lte: dateEnd } } }, - { term: { 'monitor.id': monitorId } }, - { range: { 'monitor.duration.us': { gt: 0 } } }, - ], - }, + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: dateStart, lte: dateEnd } } }, + { term: { 'monitor.id': monitorId } }, + { range: { 'monitor.duration.us': { gt: 0 } } }, + ], }, - size: 0, - aggs: { - timeseries: { - auto_date_histogram: { - field: '@timestamp', - buckets: QUERY.DEFAULT_BUCKET_COUNT, - }, - aggs: { - location: { - terms: { - field: 'observer.geo.name', - missing: 'N/A', - }, - aggs: { - duration: { stats: { field: 'monitor.duration.us' } }, - }, + }, + size: 0, + aggs: { + timeseries: { + auto_date_histogram: { + field: '@timestamp', + buckets: QUERY.DEFAULT_BUCKET_COUNT, + }, + aggs: { + location: { + terms: { + field: 'observer.geo.name', + missing: 'N/A', + }, + aggs: { + duration: { stats: { field: 'monitor.duration.us' } }, }, }, }, @@ -59,7 +56,7 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn< }, }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); const dateHistogramBuckets: any[] = result?.aggregations?.timeseries?.buckets ?? []; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index b5183ca9ffb9fc..af79126226e269 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -7,6 +7,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; import { UNNAMED_LOCATION } from '../../../common/constants'; +import { SortOptions } from '../../../../../typings/elasticsearch'; /** * Fetch data for the monitor page title. @@ -23,64 +24,65 @@ export interface GetMonitorLocationsParams { export const getMonitorLocations: UMElasticsearchQueryFn< GetMonitorLocationsParams, MonitorLocations -> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd }) => { +> = async ({ uptimeEsClient, monitorId, dateStart, dateEnd }) => { + const sortOptions: SortOptions = [ + { + '@timestamp': { + order: 'desc', + }, + }, + ]; + const params = { - index: dynamicSettings.heartbeatIndices, - body: { - size: 0, - query: { - bool: { - filter: [ - { - term: { - 'monitor.id': monitorId, - }, + size: 0, + query: { + bool: { + filter: [ + { + term: { + 'monitor.id': monitorId, }, - { - exists: { - field: 'summary', - }, + }, + { + exists: { + field: 'summary', }, - { - range: { - '@timestamp': { - gte: dateStart, - lte: dateEnd, - }, + }, + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, }, }, - ], - }, - }, - aggs: { - location: { - terms: { - field: 'observer.geo.name', - missing: '__location_missing__', }, - aggs: { - most_recent: { - top_hits: { - size: 1, - sort: { - '@timestamp': { - order: 'desc', - }, - }, - _source: ['monitor', 'summary', 'observer', '@timestamp'], - }, + ], + }, + }, + aggs: { + location: { + terms: { + field: 'observer.geo.name', + missing: '__location_missing__', + }, + aggs: { + most_recent: { + top_hits: { + size: 1, + sort: sortOptions, + _source: ['monitor', 'summary', 'observer', '@timestamp'], }, - down_history: { - sum: { - field: 'summary.down', - missing: 0, - }, + }, + down_history: { + sum: { + field: 'summary.down', + missing: 0, }, - up_history: { - sum: { - field: 'summary.up', - missing: 0, - }, + }, + up_history: { + sum: { + field: 'summary.up', + missing: 0, }, }, }, @@ -88,7 +90,8 @@ export const getMonitorLocations: UMElasticsearchQueryFn< }, }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); + const locations = result?.aggregations?.location?.buckets ?? []; const getGeo = (locGeo: { name: string; location?: string }) => { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 2ff1043d79e84d..16638f0e8cea8f 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -40,8 +40,7 @@ export const getMonitorStates: UMElasticsearchQueryFn< GetMonitorStatesParams, MonitorSummariesResult > = async ({ - callES, - dynamicSettings, + uptimeEsClient, dateRangeStart, dateRangeEnd, pagination, @@ -53,8 +52,7 @@ export const getMonitorStates: UMElasticsearchQueryFn< statusFilter = statusFilter === null ? undefined : statusFilter; const queryContext = new QueryContext( - callES, - dynamicSettings.heartbeatIndices, + uptimeEsClient, dateRangeStart, dateRangeEnd, pagination, @@ -98,52 +96,49 @@ export const getHistogramForMonitors = async ( minInterval: number ): Promise<{ [key: string]: Histogram }> => { const params = { - index: queryContext.heartbeatIndices, - body: { - size: 0, - query: { - bool: { - filter: [ - { - range: { - 'summary.down': { gt: 0 }, - }, + size: 0, + query: { + bool: { + filter: [ + { + range: { + 'summary.down': { gt: 0 }, }, - { - terms: { - 'monitor.id': monitorIds, - }, + }, + { + terms: { + 'monitor.id': monitorIds, }, - { - range: { - '@timestamp': { - gte: queryContext.dateRangeStart, - lte: queryContext.dateRangeEnd, - }, + }, + { + range: { + '@timestamp': { + gte: queryContext.dateRangeStart, + lte: queryContext.dateRangeEnd, }, }, - ], - }, - }, - aggs: { - histogram: { - date_histogram: { - field: '@timestamp', - // 12 seems to be a good size for performance given - // long monitor lists of up to 100 on the overview page - fixed_interval: minInterval + 'ms', - missing: 0, }, - aggs: { - by_id: { - terms: { - field: 'monitor.id', - size: Math.max(monitorIds.length, 1), - }, - aggs: { - totalDown: { - sum: { field: 'summary.down' }, - }, + ], + }, + }, + aggs: { + histogram: { + date_histogram: { + field: '@timestamp', + // 12 seems to be a good size for performance given + // long monitor lists of up to 100 on the overview page + fixed_interval: minInterval + 'ms', + missing: 0, + }, + aggs: { + by_id: { + terms: { + field: 'monitor.id', + size: Math.max(monitorIds.length, 1), + }, + aggs: { + totalDown: { + sum: { field: 'summary.down' }, }, }, }, @@ -151,7 +146,7 @@ export const getHistogramForMonitors = async ( }, }, }; - const { body: result } = await queryContext.search(params); + const { body: result } = await queryContext.search({ body: params }); const histoBuckets: any[] = result.aggregations?.histogram.buckets ?? []; const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index 06648d68969c14..a5121f7a7a04f5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -23,12 +23,6 @@ export interface GetMonitorStatusResult { monitorInfo: Ping; } -interface MonitorStatusKey { - monitor_id: string; - status: string; - location: string; -} - const getLocationClause = (locations: string[]) => ({ bool: { should: [ @@ -41,76 +35,80 @@ const getLocationClause = (locations: string[]) => ({ }, }); +export type AfterKey = Record<string, string | number | null> | undefined; + export const getMonitorStatus: UMElasticsearchQueryFn< GetMonitorStatusParams, GetMonitorStatusResult[] -> = async ({ callES, dynamicSettings, filters, locations, numTimes, timerange: { from, to } }) => { - let afterKey: MonitorStatusKey | undefined; +> = async ({ uptimeEsClient, filters, locations, numTimes, timerange: { from, to } }) => { + let afterKey: AfterKey; const STATUS = 'down'; let monitors: any = []; do { // today this value is hardcoded. In the future we may support // multiple status types for this alert, and this will become a parameter - const esParams: any = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter: [ - { - term: { - 'monitor.status': STATUS, - }, + const esParams = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.status': STATUS, }, - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, }, }, - // append user filters, if defined - ...(filters?.bool ? [filters] : []), - ], - }, + }, + // append user filters, if defined + ...(filters?.bool ? [filters] : []), + ], }, - size: 0, - aggs: { - monitors: { - composite: { - size: 2000, - sources: [ - { - monitorId: { - terms: { - field: 'monitor.id', - }, + }, + size: 0, + aggs: { + monitors: { + composite: { + size: 2000, + /** + * We "paginate" results by utilizing the `afterKey` field + * to tell Elasticsearch where it should start on subsequent queries. + */ + ...(afterKey ? { after: afterKey } : {}), + sources: [ + { + monitorId: { + terms: { + field: 'monitor.id', }, }, - { - status: { - terms: { - field: 'monitor.status', - }, + }, + { + status: { + terms: { + field: 'monitor.status', }, }, - { - location: { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, + }, + { + location: { + terms: { + field: 'observer.geo.name', + missing_bucket: true, }, }, - ], - }, - aggs: { - fields: { - top_hits: { - size: 1, - }, + }, + ], + }, + aggs: { + fields: { + top_hits: { + size: 1, }, }, }, @@ -122,19 +120,14 @@ export const getMonitorStatus: UMElasticsearchQueryFn< * Perform a logical `and` against the selected location filters. */ if (locations.length) { - esParams.body.query.bool.filter.push(getLocationClause(locations)); + esParams.query.bool.filter.push(getLocationClause(locations)); } - /** - * We "paginate" results by utilizing the `afterKey` field - * to tell Elasticsearch where it should start on subsequent queries. - */ - if (afterKey) { - esParams.body.aggs.monitors.composite.after = afterKey; - } + const { body: result } = await uptimeEsClient.search({ + body: esParams, + }); - const { body: result } = await callES.search(esParams); - afterKey = result?.aggregations?.monitors?.after_key; + afterKey = result?.aggregations?.monitors?.after_key as AfterKey; monitors = monitors.concat(result?.aggregations?.monitors?.buckets || []); } while (afterKey !== undefined); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 4eb2d862cb7023..325c7b0e2edefa 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMElasticsearchQueryFn } from '../adapters'; import { getFilterClause } from '../helper'; import { HistogramResult, HistogramQueryResult } from '../../../common/runtime_types'; import { QUERY } from '../../../common/constants'; import { getHistogramInterval } from '../helper/get_histogram_interval'; +import { UMElasticsearchQueryFn } from '../adapters/framework'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ @@ -26,7 +26,7 @@ export interface GetPingHistogramParams { export const getPingHistogram: UMElasticsearchQueryFn< GetPingHistogramParams, HistogramResult -> = async ({ callES, dynamicSettings, from, to, filters, monitorId, bucketSize }) => { +> = async ({ uptimeEsClient, from, to, filters, monitorId, bucketSize }) => { const boolFilters = filters ? JSON.parse(filters) : null; const additionalFilters = []; if (monitorId) { @@ -40,34 +40,31 @@ export const getPingHistogram: UMElasticsearchQueryFn< const minInterval = getHistogramInterval(from, to, QUERY.DEFAULT_BUCKET_COUNT); const params = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - bool: { - filter, - }, + query: { + bool: { + filter, }, - size: 0, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize || minInterval + 'ms', - missing: 0, - }, - aggs: { - down: { - filter: { - term: { - 'monitor.status': 'down', - }, + }, + size: 0, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize || minInterval + 'ms', + missing: 0, + }, + aggs: { + down: { + filter: { + term: { + 'monitor.status': 'down', }, }, - up: { - filter: { - term: { - 'monitor.status': 'up', - }, + }, + up: { + filter: { + term: { + 'monitor.status': 'up', }, }, }, @@ -76,7 +73,7 @@ export const getPingHistogram: UMElasticsearchQueryFn< }, }; - const { body: result } = await callES.search(params); + const { body: result } = await uptimeEsClient.search({ body: params }); const buckets: HistogramQueryResult[] = result?.aggregations?.timeseries?.buckets ?? []; const histogram = buckets.map((bucket) => { const x: number = bucket.key; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index e72b16de3d66f4..3b852ad1a73817 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -52,8 +52,7 @@ const REMOVE_NON_SUMMARY_BROWSER_CHECKS = { }; export const getPings: UMElasticsearchQueryFn<GetPingsParams, PingsResponse> = async ({ - callES, - dynamicSettings, + uptimeEsClient, dateRange: { from, to }, index, monitorId, @@ -63,56 +62,39 @@ export const getPings: UMElasticsearchQueryFn<GetPingsParams, PingsResponse> = a location, }) => { const size = sizeParam ?? DEFAULT_PAGE_SIZE; - const sortParam = { sort: [{ '@timestamp': { order: sort ?? 'desc' } }] }; - const filter: any[] = [{ range: { '@timestamp': { gte: from, lte: to } } }]; - if (monitorId) { - filter.push({ term: { 'monitor.id': monitorId } }); - } - if (status) { - filter.push({ term: { 'monitor.status': status } }); - } - let postFilterClause = {}; - if (location) { - postFilterClause = { post_filter: { term: { 'observer.geo.name': location } } }; - } - const queryContext = { - bool: { - filter, - ...REMOVE_NON_SUMMARY_BROWSER_CHECKS, - }, - }; - const params: any = { - index: dynamicSettings.heartbeatIndices, - body: { - query: { - ...queryContext, + const searchBody = { + size, + ...(index ? { from: index * size } : {}), + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: from, lte: to } } }, + ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), + ...(status ? [{ term: { 'monitor.status': status } }] : []), + ], + ...REMOVE_NON_SUMMARY_BROWSER_CHECKS, }, - ...sortParam, - size, - aggregations: { - locations: { - terms: { - field: 'observer.geo.name', - missing: 'N/A', - size: 1000, - }, + }, + sort: [{ '@timestamp': { order: (sort ?? 'desc') as 'asc' | 'desc' } }], + aggs: { + locations: { + terms: { + field: 'observer.geo.name', + missing: 'N/A', + size: 1000, }, }, - ...postFilterClause, }, + ...(location ? { post_filter: { term: { 'observer.geo.name': location } } } : {}), }; - if (index) { - params.body.from = index * size; - } - const { body: { hits: { hits, total }, aggregations: aggs, }, - } = await callES.search(params); + } = await uptimeEsClient.search({ body: searchBody }); const locations = aggs?.locations ?? { buckets: [{ key: 'N/A', doc_count: 0 }] }; @@ -131,7 +113,7 @@ export const getPings: UMElasticsearchQueryFn<GetPingsParams, PingsResponse> = a return { total: total.value, - locations: locations.buckets.map((bucket: { key: string }) => bucket.key), + locations: locations.buckets.map((bucket) => bucket.key as string), pings, }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index ac36585ff09397..9df20c79a61066 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -16,15 +16,13 @@ export interface GetSnapshotCountParams { } export const getSnapshotCount: UMElasticsearchQueryFn<GetSnapshotCountParams, Snapshot> = async ({ - callES, - dynamicSettings: { heartbeatIndices }, + uptimeEsClient, dateRangeStart, dateRangeEnd, filters, }): Promise<Snapshot> => { const context = new QueryContext( - callES, - heartbeatIndices, + uptimeEsClient, dateRangeStart, dateRangeEnd, CONTEXT_DEFAULTS.CURSOR_PAGINATION, @@ -40,7 +38,6 @@ export const getSnapshotCount: UMElasticsearchQueryFn<GetSnapshotCountParams, Sn const statusCount = async (context: QueryContext): Promise<Snapshot> => { const { body: res } = await context.search({ - index: context.heartbeatIndices, body: statusCountBody(await context.dateAndCustomFilters()), }); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index e53fff429dd8df..b4de286a5b92de 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -19,9 +19,7 @@ describe(QueryContext, () => { }; let qc: QueryContext; - beforeEach( - () => (qc = new QueryContext({}, 'indexName', rangeStart, rangeEnd, pagination, null, 10)) - ); + beforeEach(() => (qc = new QueryContext({}, rangeStart, rangeEnd, pagination, null, 10))); describe('dateRangeFilter()', () => { const expectedRange = { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index 40775bde1c7f5a..205b283d40d6a6 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -8,13 +8,6 @@ import { CursorPagination } from '../types'; import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; import { QueryContext } from '../query_context'; -export const prevPagination = (key: any): CursorPagination => { - return { - cursorDirection: CursorDirection.BEFORE, - sortOrder: SortOrder.ASC, - cursorKey: key, - }; -}; export const nextPagination = (key: any): CursorPagination => { return { cursorDirection: CursorDirection.AFTER, @@ -23,14 +16,5 @@ export const nextPagination = (key: any): CursorPagination => { }; }; export const simpleQueryContext = (): QueryContext => { - return new QueryContext( - undefined, - 'indexName', - '', - '', - nextPagination('something'), - undefined, - 0, - '' - ); + return new QueryContext(undefined, '', '', nextPagination('something'), undefined, 0, ''); }; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 38e7dabb19941b..2331d991e3af36 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -36,7 +36,6 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number) const body = await queryBody(queryContext, searchAfter, size); const params = { - index: queryContext.heartbeatIndices, body, }; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index 96df8ea651c44a..bcfb3035920fb9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -12,7 +12,6 @@ import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; export class QueryContext { callES: ElasticsearchClient; - heartbeatIndices: string; dateRangeStart: string; dateRangeEnd: string; pagination: CursorPagination; @@ -23,7 +22,6 @@ export class QueryContext { constructor( database: any, - heartbeatIndices: string, dateRangeStart: string, dateRangeEnd: string, pagination: CursorPagination, @@ -32,7 +30,6 @@ export class QueryContext { statusFilter?: string ) { this.callES = database; - this.heartbeatIndices = heartbeatIndices; this.dateRangeStart = dateRangeStart; this.dateRangeEnd = dateRangeEnd; this.pagination = pagination; @@ -42,12 +39,10 @@ export class QueryContext { } async search(params: any): Promise<any> { - params.index = this.heartbeatIndices; - return this.callES.search(params); + return this.callES.search({ body: params.body }); } async count(params: any): Promise<any> { - params.index = this.heartbeatIndices; return this.callES.count(params); } @@ -138,7 +133,6 @@ export class QueryContext { clone(): QueryContext { return new QueryContext( this.callES, - this.heartbeatIndices, this.dateRangeStart, this.dateRangeEnd, this.pagination, diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 6be9f813016f80..dc3af2805d13f7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -109,7 +109,6 @@ export const query = async ( potentialMatchMonitorIDs: string[] ): Promise<any> => { const params = { - index: queryContext.heartbeatIndices, body: { size: 0, query: { diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 2eec9e233f5d27..a9191dde3df4c1 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -48,7 +48,7 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = { getUptimeDynamicSettings: async (client): Promise<DynamicSettings> => { try { const obj = await client.get<DynamicSettings>(umDynamicSettings.name, settingsObjectId); - return obj.attributes; + return obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULTS; } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { return DYNAMIC_SETTINGS_DEFAULTS; diff --git a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts index fb22d603a2d56e..d377095a2a3707 100644 --- a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts +++ b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts @@ -30,7 +30,7 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = direction: schema.maybe(schema.string()), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const index = request.query?.index ?? 0; const size = request.query?.size ?? DEFAULT_SIZE; const from = request.query?.from ?? DEFAULT_FROM; @@ -39,8 +39,7 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = const direction = request.query?.direction ?? DEFAULT_DIRECTION; const { search } = request.query; const result = await libs.requests.getCerts({ - callES, - dynamicSettings, + uptimeEsClient, index, search, size, diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index 98e995173d8117..f7be9e10d1004e 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -20,7 +20,9 @@ export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSer method: 'GET', path: '/api/uptime/dynamic_settings', validate: false, - handler: async ({ dynamicSettings }, _context, _request, response): Promise<any> => { + handler: async ({ savedObjectsClient }, _context, _request, response): Promise<any> => { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); + return response.ok({ body: dynamicSettings, }); diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 418cde9e701d50..5c1be4cdd81436 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -12,13 +12,12 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer method: 'GET', path: API_URLS.INDEX_PATTERN, validate: false, - handler: async ({ callES, dynamicSettings }, _context, _request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, _request, response): Promise<any> => { try { return response.ok({ body: { ...(await libs.requests.getIndexPattern({ - esClient: callES, - dynamicSettings, + uptimeEsClient, })), }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index 9a4280efa98f92..e57643aed7e36d 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -12,11 +12,13 @@ export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerL method: 'GET', path: API_URLS.INDEX_STATUS, validate: false, - handler: async ({ callES, dynamicSettings }, _context, _request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, _request, response): Promise<any> => { try { return response.ok({ body: { - ...(await libs.requests.getIndexStatus({ callES, dynamicSettings })), + ...(await libs.requests.getIndexStatus({ + uptimeEsClient, + })), }, }); } catch (e) { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 7b461060bf4bce..3eac49341b2c2b 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -24,7 +24,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ options: { tags: ['access:uptime-read'], }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { try { const { dateRangeStart, @@ -42,10 +42,9 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ indexStatus, { summaries, nextPagePagination, prevPagePagination }, ] = await Promise.all([ - libs.requests.getIndexStatus({ callES, dynamicSettings }), + libs.requests.getIndexStatus({ uptimeEsClient }), libs.requests.getMonitorStates({ - callES, - dynamicSettings, + uptimeEsClient, dateRangeStart, dateRangeEnd, pagination: decodedPagination, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index a110209043a7e7..e2dbf7114fc917 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -19,14 +19,13 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe dateEnd: schema.string(), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { ...(await libs.requests.getMonitorLocations({ - callES, - dynamicSettings, + uptimeEsClient, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index bb002f8a8c286c..3d47f0eab8640b 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -20,11 +20,10 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib dateEnd: schema.string(), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const { monitorId, dateStart, dateEnd } = request.query; const result = await libs.requests.getLatestMonitor({ - callES, - dynamicSettings, + uptimeEsClient, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index bb54effc0d57e8..0982fc19866049 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -19,7 +19,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ dateEnd: schema.maybe(schema.string()), }), }, - handler: async ({ callES, dynamicSettings }, context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, context, request, response): Promise<any> => { const { monitorId, dateStart, dateEnd } = request.query; const alertsClient = context.alerting?.getAlertsClient(); @@ -27,8 +27,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ return response.ok({ body: { ...(await libs.requests.getMonitorDetails({ - callES, - dynamicSettings, + uptimeEsClient, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index a4e024e795b46a..eec3fdf9e72575 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -20,14 +20,13 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer dateEnd: schema.string(), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { ...(await libs.requests.getMonitorDurationChart({ - callES, - dynamicSettings, + uptimeEsClient, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index 00cbaf0d16723c..163fbd4f8dd6e0 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -28,7 +28,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi tags: arrayOrStringType, }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response) => { + handler: async ({ uptimeEsClient }, _context, request, response) => { const { dateRangeStart, dateRangeEnd, locations, schemes, search, ports, tags } = request.query; let parsedSearch: Record<string, any> | undefined; @@ -41,8 +41,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi } const filtersResponse = await libs.requests.getFilterBar({ - callES, - dynamicSettings, + uptimeEsClient, dateRangeStart, dateRangeEnd, search: parsedSearch, diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index 4ac50d0e78c4cf..ba36b171793b71 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -21,12 +21,11 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe bucketSize: schema.maybe(schema.string()), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const { dateStart, dateEnd, monitorId, filters, bucketSize } = request.query; const result = await libs.requests.getPingHistogram({ - callES, - dynamicSettings, + uptimeEsClient, from: dateStart, to: dateEnd, monitorId, diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index d97195a7fe2b17..d00386d06c74c2 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -27,7 +27,7 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = status: schema.maybe(schema.string()), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const { from, to, ...optional } = request.query; const params = GetPingsParamsType.decode({ dateRange: { from, to }, ...optional }); if (isLeft(params)) { @@ -37,8 +37,7 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = } const result = await libs.requests.getPings({ - callES, - dynamicSettings, + uptimeEsClient, ...params.right, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index 1fc52dd24f9d07..161d7dd558fdbd 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -17,11 +17,10 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ stepIndex: schema.number(), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response) => { + handler: async ({ uptimeEsClient }, _context, request, response) => { const { checkGroup, stepIndex } = request.params; const result = await libs.requests.getJourneyScreenshot({ - callES, - dynamicSettings, + uptimeEsClient, checkGroup, stepIndex, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index b6e06850ad3b60..5b8583ea0322fe 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -16,11 +16,10 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => checkGroup: schema.string(), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response) => { + handler: async ({ uptimeEsClient }, _context, request, response) => { const { checkGroup } = request.params; const result = await libs.requests.getJourneySteps({ - callES, - dynamicSettings, + uptimeEsClient, checkGroup, }); diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 9502335e4e5a8f..224ef87fd90afb 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -19,11 +19,10 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs filters: schema.maybe(schema.string()), }), }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => { const { dateRangeStart, dateRangeEnd, filters } = request.query; const result = await libs.requests.getSnapshotCount({ - callES, - dynamicSettings, + uptimeEsClient, dateRangeStart, dateRangeEnd, filters, diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 9881d41f3bf2bd..85f274c96cf9a8 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -24,7 +24,7 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ }), }, handler: async ( - { savedObjectsClient, callES, dynamicSettings }, + { savedObjectsClient, uptimeEsClient }, _context, request, response @@ -33,7 +33,10 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ if (pageView.refreshTelemetryHistory) { KibanaTelemetryAdapter.clearLocalTelemetry(); } - await KibanaTelemetryAdapter.countNoOfUniqueMonitorAndLocations(callES, savedObjectsClient); + await KibanaTelemetryAdapter.countNoOfUniqueMonitorAndLocations( + uptimeEsClient, + savedObjectsClient + ); const pageViewResult = KibanaTelemetryAdapter.countPageView(pageView as PageViewParams); return response.ok({ diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index 5e5f4a2a991cfd..df1762a3b318d7 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -15,10 +15,8 @@ import { KibanaResponseFactory, IKibanaResponse, IScopedClusterClient, - ElasticsearchClient, } from 'kibana/server'; -import { DynamicSettings } from '../../common/runtime_types'; -import { UMServerLibs } from '../lib/lib'; +import { UMServerLibs, UptimeESClient } from '../lib/lib'; /** * Defines the basic properties employed by Uptime routes. @@ -64,9 +62,8 @@ export type UMKibanaRouteWrapper = (uptimeRoute: UptimeRoute) => UMKibanaRoute; * This type can store custom parameters used by the internal Uptime route handlers. */ export interface UMRouteParams { - callES: ElasticsearchClient; + uptimeEsClient: UptimeESClient; esClient: IScopedClusterClient; - dynamicSettings: DynamicSettings; savedObjectsClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index b2f1c7d6424e62..a1cf3c05e2de30 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -5,7 +5,7 @@ */ import { UMKibanaRouteWrapper } from './types'; -import { savedObjectsAdapter } from '../lib/saved_objects'; +import { createUptimeESClient } from '../lib/lib'; export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ ...uptimeRoute, @@ -15,9 +15,14 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ handler: async (context, request, response) => { const { client: esClient } = context.core.elasticsearch; const { client: savedObjectsClient } = context.core.savedObjects; - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); + + const uptimeEsClient = createUptimeESClient({ + savedObjectsClient, + esClient: esClient.asCurrentUser, + }); + return uptimeRoute.handler( - { callES: esClient.asCurrentUser, esClient, savedObjectsClient, dynamicSettings }, + { uptimeEsClient, esClient, savedObjectsClient }, context, request, response diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 7d4cc41cfbe5a3..505ad3c7d866b3 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/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 937045b6218c6d..d3cd3db124ecde 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -17,7 +17,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - describe('eventLog', () => { + // FLAKY: https://github.com/elastic/kibana/issues/81668 + describe.skip('eventLog', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); 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 c2dfd28d5c844f..0137a90ce98170 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/apis/telemetry/telemetry.js b/x-pack/test/api_integration/apis/telemetry/telemetry.js index d0b7b2bbbb7d23..b3c0473b534cef 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.js @@ -77,7 +77,7 @@ export default function ({ getService }) { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); expect(body).length(4); @@ -100,7 +100,7 @@ export default function ({ getService }) { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); expect(body).length(2); diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js index a186e6561e4f94..6a5a7db4d2560e 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -45,12 +45,10 @@ export default function ({ getService }) { }); it('should pull local stats and validate data types', async () => { - const timestamp = '2018-07-23T22:13:00Z'; - const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); expect(body.length).to.be(1); @@ -102,12 +100,10 @@ export default function ({ getService }) { }); it('should pull local stats and validate fields', async () => { - const timestamp = '2018-07-23T22:13:00Z'; - const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) + .send({ unencrypted: true }) .expect(200); const stats = body[0]; diff --git a/x-pack/test/api_integration/services/usage_api.ts b/x-pack/test/api_integration/services/usage_api.ts index b4adc6c61b6641..4d1181b2436f60 100644 --- a/x-pack/test/api_integration/services/usage_api.ts +++ b/x-pack/test/api_integration/services/usage_api.ts @@ -39,7 +39,6 @@ export function UsageAPIProvider({ getService }: FtrProviderContext) { */ async getTelemetryStats(payload: { unencrypted?: boolean; - timestamp: number | string; }): Promise<ReturnType<TelemetryCollectionManagerPlugin['getStats']>> { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap new file mode 100644 index 00000000000000..4bf242d8f9b6d0 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap @@ -0,0 +1,824 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UX page load dist when there is data returns page load distribution 1`] = ` +Object { + "maxDuration": 54.46, + "minDuration": 0, + "pageLoadDistribution": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0.5, + "y": 0, + }, + Object { + "x": 1, + "y": 0, + }, + Object { + "x": 1.5, + "y": 0, + }, + Object { + "x": 2, + "y": 0, + }, + Object { + "x": 2.5, + "y": 0, + }, + Object { + "x": 3, + "y": 16.6666666666667, + }, + Object { + "x": 3.5, + "y": 0, + }, + Object { + "x": 4, + "y": 0, + }, + Object { + "x": 4.5, + "y": 0, + }, + Object { + "x": 5, + "y": 50, + }, + Object { + "x": 5.5, + "y": 0, + }, + Object { + "x": 6, + "y": 0, + }, + Object { + "x": 6.5, + "y": 0, + }, + Object { + "x": 7, + "y": 0, + }, + Object { + "x": 7.5, + "y": 0, + }, + Object { + "x": 8, + "y": 0, + }, + Object { + "x": 8.5, + "y": 0, + }, + Object { + "x": 9, + "y": 0, + }, + Object { + "x": 9.5, + "y": 0, + }, + Object { + "x": 10, + "y": 0, + }, + Object { + "x": 10.5, + "y": 0, + }, + Object { + "x": 11, + "y": 0, + }, + Object { + "x": 11.5, + "y": 0, + }, + Object { + "x": 12, + "y": 0, + }, + Object { + "x": 12.5, + "y": 0, + }, + Object { + "x": 13, + "y": 0, + }, + Object { + "x": 13.5, + "y": 0, + }, + Object { + "x": 14, + "y": 0, + }, + Object { + "x": 14.5, + "y": 0, + }, + Object { + "x": 15, + "y": 0, + }, + Object { + "x": 15.5, + "y": 0, + }, + Object { + "x": 16, + "y": 0, + }, + Object { + "x": 16.5, + "y": 0, + }, + Object { + "x": 17, + "y": 0, + }, + Object { + "x": 17.5, + "y": 0, + }, + Object { + "x": 18, + "y": 0, + }, + Object { + "x": 18.5, + "y": 0, + }, + Object { + "x": 19, + "y": 0, + }, + Object { + "x": 19.5, + "y": 0, + }, + Object { + "x": 20, + "y": 0, + }, + Object { + "x": 20.5, + "y": 0, + }, + Object { + "x": 21, + "y": 0, + }, + Object { + "x": 21.5, + "y": 0, + }, + Object { + "x": 22, + "y": 0, + }, + Object { + "x": 22.5, + "y": 0, + }, + Object { + "x": 23, + "y": 0, + }, + Object { + "x": 23.5, + "y": 0, + }, + Object { + "x": 24, + "y": 0, + }, + Object { + "x": 24.5, + "y": 0, + }, + Object { + "x": 25, + "y": 0, + }, + Object { + "x": 25.5, + "y": 0, + }, + Object { + "x": 26, + "y": 0, + }, + Object { + "x": 26.5, + "y": 0, + }, + Object { + "x": 27, + "y": 0, + }, + Object { + "x": 27.5, + "y": 0, + }, + Object { + "x": 28, + "y": 0, + }, + Object { + "x": 28.5, + "y": 0, + }, + Object { + "x": 29, + "y": 0, + }, + Object { + "x": 29.5, + "y": 0, + }, + Object { + "x": 30, + "y": 0, + }, + Object { + "x": 30.5, + "y": 0, + }, + Object { + "x": 31, + "y": 0, + }, + Object { + "x": 31.5, + "y": 0, + }, + Object { + "x": 32, + "y": 0, + }, + Object { + "x": 32.5, + "y": 0, + }, + Object { + "x": 33, + "y": 0, + }, + Object { + "x": 33.5, + "y": 0, + }, + Object { + "x": 34, + "y": 0, + }, + Object { + "x": 34.5, + "y": 0, + }, + Object { + "x": 35, + "y": 0, + }, + Object { + "x": 35.5, + "y": 0, + }, + Object { + "x": 36, + "y": 0, + }, + Object { + "x": 36.5, + "y": 0, + }, + Object { + "x": 37, + "y": 0, + }, + Object { + "x": 37.5, + "y": 16.6666666666667, + }, + Object { + "x": 38, + "y": 0, + }, + Object { + "x": 38.5, + "y": 0, + }, + Object { + "x": 39, + "y": 0, + }, + Object { + "x": 39.5, + "y": 0, + }, + Object { + "x": 40, + "y": 0, + }, + Object { + "x": 40.5, + "y": 0, + }, + Object { + "x": 41, + "y": 0, + }, + Object { + "x": 41.5, + "y": 0, + }, + Object { + "x": 42, + "y": 0, + }, + Object { + "x": 42.5, + "y": 0, + }, + Object { + "x": 43, + "y": 0, + }, + Object { + "x": 43.5, + "y": 0, + }, + Object { + "x": 44, + "y": 0, + }, + Object { + "x": 44.5, + "y": 0, + }, + Object { + "x": 45, + "y": 0, + }, + Object { + "x": 45.5, + "y": 0, + }, + Object { + "x": 46, + "y": 0, + }, + Object { + "x": 46.5, + "y": 0, + }, + Object { + "x": 47, + "y": 0, + }, + Object { + "x": 47.5, + "y": 0, + }, + Object { + "x": 48, + "y": 0, + }, + Object { + "x": 48.5, + "y": 0, + }, + Object { + "x": 49, + "y": 0, + }, + Object { + "x": 49.5, + "y": 0, + }, + Object { + "x": 50, + "y": 0, + }, + Object { + "x": 50.5, + "y": 0, + }, + Object { + "x": 51, + "y": 0, + }, + Object { + "x": 51.5, + "y": 0, + }, + Object { + "x": 52, + "y": 0, + }, + Object { + "x": 52.5, + "y": 0, + }, + Object { + "x": 53, + "y": 0, + }, + Object { + "x": 53.5, + "y": 0, + }, + Object { + "x": 54, + "y": 0, + }, + Object { + "x": 54.5, + "y": 16.6666666666667, + }, + ], + "percentiles": Object { + "50.0": 4.88, + "75.0": 37.09, + "90.0": 37.09, + "95.0": 54.46, + "99.0": 54.46, + }, +} +`; + +exports[`UX page load dist when there is data returns page load distribution with breakdown 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0.5, + "y": 0, + }, + Object { + "x": 1, + "y": 0, + }, + Object { + "x": 1.5, + "y": 0, + }, + Object { + "x": 2, + "y": 0, + }, + Object { + "x": 2.5, + "y": 0, + }, + Object { + "x": 3, + "y": 25, + }, + Object { + "x": 3.5, + "y": 0, + }, + Object { + "x": 4, + "y": 0, + }, + Object { + "x": 4.5, + "y": 0, + }, + Object { + "x": 5, + "y": 25, + }, + Object { + "x": 5.5, + "y": 0, + }, + Object { + "x": 6, + "y": 0, + }, + Object { + "x": 6.5, + "y": 0, + }, + Object { + "x": 7, + "y": 0, + }, + Object { + "x": 7.5, + "y": 0, + }, + Object { + "x": 8, + "y": 0, + }, + Object { + "x": 8.5, + "y": 0, + }, + Object { + "x": 9, + "y": 0, + }, + Object { + "x": 9.5, + "y": 0, + }, + Object { + "x": 10, + "y": 0, + }, + Object { + "x": 10.5, + "y": 0, + }, + Object { + "x": 11, + "y": 0, + }, + Object { + "x": 11.5, + "y": 0, + }, + Object { + "x": 12, + "y": 0, + }, + Object { + "x": 12.5, + "y": 0, + }, + Object { + "x": 13, + "y": 0, + }, + Object { + "x": 13.5, + "y": 0, + }, + Object { + "x": 14, + "y": 0, + }, + Object { + "x": 14.5, + "y": 0, + }, + Object { + "x": 15, + "y": 0, + }, + Object { + "x": 15.5, + "y": 0, + }, + Object { + "x": 16, + "y": 0, + }, + Object { + "x": 16.5, + "y": 0, + }, + Object { + "x": 17, + "y": 0, + }, + Object { + "x": 17.5, + "y": 0, + }, + Object { + "x": 18, + "y": 0, + }, + Object { + "x": 18.5, + "y": 0, + }, + Object { + "x": 19, + "y": 0, + }, + Object { + "x": 19.5, + "y": 0, + }, + Object { + "x": 20, + "y": 0, + }, + Object { + "x": 20.5, + "y": 0, + }, + Object { + "x": 21, + "y": 0, + }, + Object { + "x": 21.5, + "y": 0, + }, + Object { + "x": 22, + "y": 0, + }, + Object { + "x": 22.5, + "y": 0, + }, + Object { + "x": 23, + "y": 0, + }, + Object { + "x": 23.5, + "y": 0, + }, + Object { + "x": 24, + "y": 0, + }, + Object { + "x": 24.5, + "y": 0, + }, + Object { + "x": 25, + "y": 0, + }, + Object { + "x": 25.5, + "y": 0, + }, + Object { + "x": 26, + "y": 0, + }, + Object { + "x": 26.5, + "y": 0, + }, + Object { + "x": 27, + "y": 0, + }, + Object { + "x": 27.5, + "y": 0, + }, + Object { + "x": 28, + "y": 0, + }, + Object { + "x": 28.5, + "y": 0, + }, + Object { + "x": 29, + "y": 0, + }, + Object { + "x": 29.5, + "y": 0, + }, + Object { + "x": 30, + "y": 0, + }, + Object { + "x": 30.5, + "y": 0, + }, + Object { + "x": 31, + "y": 0, + }, + Object { + "x": 31.5, + "y": 0, + }, + Object { + "x": 32, + "y": 0, + }, + Object { + "x": 32.5, + "y": 0, + }, + Object { + "x": 33, + "y": 0, + }, + Object { + "x": 33.5, + "y": 0, + }, + Object { + "x": 34, + "y": 0, + }, + Object { + "x": 34.5, + "y": 0, + }, + Object { + "x": 35, + "y": 0, + }, + Object { + "x": 35.5, + "y": 0, + }, + Object { + "x": 36, + "y": 0, + }, + Object { + "x": 36.5, + "y": 0, + }, + Object { + "x": 37, + "y": 0, + }, + Object { + "x": 37.5, + "y": 25, + }, + ], + "name": "Chrome", + }, + Object { + "data": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0.5, + "y": 0, + }, + Object { + "x": 1, + "y": 0, + }, + Object { + "x": 1.5, + "y": 0, + }, + Object { + "x": 2, + "y": 0, + }, + Object { + "x": 2.5, + "y": 0, + }, + Object { + "x": 3, + "y": 0, + }, + Object { + "x": 3.5, + "y": 0, + }, + Object { + "x": 4, + "y": 0, + }, + Object { + "x": 4.5, + "y": 0, + }, + Object { + "x": 5, + "y": 100, + }, + ], + "name": "Chrome Mobile", + }, +] +`; + +exports[`UX page load dist when there is no data returns empty list 1`] = `Object {}`; + +exports[`UX page load dist when there is no data returns empty list with breakdowns 1`] = `Object {}`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.ts new file mode 100644 index 00000000000000..fa5fcbcb6a7c3c --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('UX page load dist', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + it('returns empty list with breakdowns', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution/breakdown?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdown=Browser' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns page load distribution', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + it('returns page load distribution with breakdown', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution/breakdown?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdown=Browser' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 97ab662313c7c7..ca7f6627842db7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -37,6 +37,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/page_views.ts')); loadTestFile(require.resolve('./csm/js_errors.ts')); loadTestFile(require.resolve('./csm/has_rum_data.ts')); + loadTestFile(require.resolve('./csm/page_load_dist.ts')); }); }); } 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 5fb6f21c51c95a..6ab29ffa09e130 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 c67eda1d3a16b6..180fc62d3d39ab 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 9c3a85e99c29d4..e77405f3cd49b0 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 3176841b009d40..ca24f0d2e32c59 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 0c7ab52abf8c87..d26e31394b9f56 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 73d17b985216af..ac64818fe629e1 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 17814868fecc09..b119c71664f593 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 80cf2c8199807d..3cf0d6892377ef 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 92ef544ee9b379..6949052df47030 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 7a351d09b5b9f4..9a45dd541bb562 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 d2262c684dc6da..a1e7f9a7fa89e1 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 c682c1f1f46402..b653d469055035 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 53a8f1f4ca5c0b..a8a5f2abd072b9 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 6c3b1c45e202ec..73be4154db1ebd 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 7104e16f438c6d..786e9538432108 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 35b31d2ccfefa1..66aa43e8a38172 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 2610796bdc384b..4f76a0544a152a 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 f496d035d8e606..2f06a84c7223bc 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 9c20d58c5f4e58..fe80402b607312 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 1bbfce42d2baad..c72b2e50b39fcf 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 c6294cfe6ec28b..f5774e09bb5e9a 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 556217877968b6..f70720cc752b26 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 a84d9845085e0a..f8a25b0081ef96 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 36a9649d875cac..28ea2e1ff88034 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 69330a2bf682a2..e32771d0d917c7 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 cfccb7436ea207..1697554441c16a 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 2f5a043881eeb0..d8e9c650c81169 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 22aa40b0721a43..c5b65039aa1164 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 d473863e7d028b..bbd85e353e0955 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 c889e152759a8e..b653d469055035 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 651a7601ca95a8..7e4a6ad86cda5c 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 a18faf8543042e..0da12ebba055a0 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 58790dbfb759c7..7ea47312a50302 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 36cd8480998c56..21cfab3db6d6a3 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 7104e16f438c6d..786e9538432108 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 35b31d2ccfefa1..66aa43e8a38172 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 00000000000000..d6beb912d70075 --- /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 00000000000000..09cc470defa081 --- /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 00000000000000..e29487880de6b6 --- /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 00000000000000..d68f0f6a69277e --- /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 00000000000000..d2aca34e27399a --- /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 00000000000000..9b38f0f7cbb42b --- /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 00000000000000..c3537efc12de77 --- /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 00000000000000..0c227c9acc38c8 --- /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 00000000000000..5c110996c21984 --- /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 00000000000000..d2066b1023d3c2 --- /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 2610796bdc384b..4f76a0544a152a 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 f496d035d8e606..2f06a84c7223bc 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 fac1fbaaf96758..8bb4c45d91bdd6 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 f76bdb4ebc718d..0db3013503a33f 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 1bbfce42d2baad..c72b2e50b39fcf 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 664077d5a4fab9..4ae953ead9df7e 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 962ae53b1241f0..97d5b079fd2069 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 bbc3943b75955b..87e3b145ed6fd6 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 dbe66741e06c7b..4de8abefe16fc1 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 69330a2bf682a2..e32771d0d917c7 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 cfccb7436ea207..1697554441c16a 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 23a8776b14631d..59dbe97557157b 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 22aa40b0721a43..c5b65039aa1164 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 f458fe118dcf7d..06d33da8f1f555 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/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 0cb998b9b7c35e..332a66d3503385 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -16,6 +16,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_overrides')); loadTestFile(require.resolve('./install_prerelease')); loadTestFile(require.resolve('./install_remove_assets')); + loadTestFile(require.resolve('./install_remove_multiple')); loadTestFile(require.resolve('./install_update')); loadTestFile(require.resolve('./bulk_upgrade')); loadTestFile(require.resolve('./update_assets')); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts index 82072f59a482b9..1e7612455a32c1 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts @@ -5,6 +5,8 @@ */ import expect from '@kbn/expect'; +import path from 'path'; +import fs from 'fs'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -22,6 +24,23 @@ export default function (providerContext: FtrProviderContext) { const experimental2PkgName = 'experimental2'; const experimental2PkgKey = `${experimental2PkgName}-${pkgVersion}`; + const uploadPkgKey = 'apache-0.1.4'; + + const installUploadPackage = async (pkg: string) => { + const buf = fs.readFileSync(testPkgArchiveZip); + await supertest + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/zip') + .send(buf) + .expect(200); + }; + + const testPkgArchiveZip = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.zip' + ); + const uninstallPackage = async (pkg: string) => { await supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); }; @@ -40,11 +59,7 @@ export default function (providerContext: FtrProviderContext) { const uninstallingPackagesPromise = pkgs.map((pkg) => uninstallPackage(pkg)); return Promise.all(uninstallingPackagesPromise); }; - const expectPkgFieldToExist = async ( - fields: any[], - fieldName: string, - exists: boolean = true - ) => { + const expectPkgFieldToExist = (fields: any[], fieldName: string, exists: boolean = true) => { const fieldExists = fields.find((field: { name: string }) => field.name === fieldName); if (exists) { expect(fieldExists).not.to.be(undefined); @@ -57,12 +72,13 @@ export default function (providerContext: FtrProviderContext) { before(async () => { if (!server.enabled) return; await installPackages([pkgKey, experimentalPkgKey, experimental2PkgKey]); + await installUploadPackage(uploadPkgKey); }); after(async () => { if (!server.enabled) return; - await uninstallPackages([pkgKey, experimentalPkgKey, experimental2PkgKey]); + await uninstallPackages([pkgKey, experimentalPkgKey]); }); - it('should create index patterns from all installed packages, experimental or beta', async () => { + it('should create index patterns from all installed packages: uploaded, experimental, beta', async () => { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'logs-*', @@ -73,6 +89,7 @@ export default function (providerContext: FtrProviderContext) { expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); expectPkgFieldToExist(fieldsLogs, 'logs_experimental2_name'); + expectPkgFieldToExist(fieldsLogs, 'apache.error.uploadtest'); const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'metrics-*', @@ -81,6 +98,7 @@ export default function (providerContext: FtrProviderContext) { expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name'); + expectPkgFieldToExist(fieldsMetrics, 'apache.status.uploadtest'); }); it('should correctly recreate index patterns when a package is uninstalled', async () => { await uninstallPackage(experimental2PkgKey); @@ -88,10 +106,11 @@ export default function (providerContext: FtrProviderContext) { type: 'index-pattern', id: 'logs-*', }); - const fields = JSON.parse(resIndexPatternLogs.attributes.fields); - expectPkgFieldToExist(fields, 'logs_test_name'); - expectPkgFieldToExist(fields, 'logs_experimental_name'); - expectPkgFieldToExist(fields, 'logs_experimental2_name', false); + const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); + expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental2_name', false); + expectPkgFieldToExist(fieldsLogs, 'apache.error.uploadtest'); const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'metrics-*', @@ -101,6 +120,27 @@ export default function (providerContext: FtrProviderContext) { expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name', false); + expectPkgFieldToExist(fieldsMetrics, 'apache.status.uploadtest'); + }); + it('should correctly recreate index patterns when an uploaded package is uninstalled', async () => { + await uninstallPackage(uploadPkgKey); + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); + expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); + expectPkgFieldToExist(fieldsLogs, 'apache.error.uploadtest', false); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + + expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); + expectPkgFieldToExist(fieldsMetrics, 'apache.status.uploadtest', false); }); }); } 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 b1f2ac6797fb38..c5d3607e05cb8c 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 2095ed0dba3456..6a8a12b2f2d494 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/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts index 288804750277ea..768bfb3a69fdf1 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/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 7637e573e8e393..f973831a313ca6 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATES } from './constants'; @@ -64,7 +63,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set(COMMON_REQUEST_HEADERS) .set('Accept', 'application/json') .send({ - timestamp: moment().toISOString(), unencrypted: true, }) .expect(200) @@ -85,7 +83,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set(COMMON_REQUEST_HEADERS) .set('Accept', 'application/json') .send({ - timestamp: moment().toISOString(), unencrypted: true, }) .expect(200) diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index b0ef147b40f2e0..22b291c36739ae 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import moment from 'moment'; import { DATES } from './constants'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -118,7 +117,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set(COMMON_REQUEST_HEADERS) .set('Accept', 'application/json') .send({ - timestamp: moment().toISOString(), unencrypted: true, }) .expect(200) diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index 467a33fb018546..1d40a0579ccd1c 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -68,10 +68,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('keeps selected index pattern after refresh', async () => { - await PageObjects.lens.switchDataPanelIndexPattern('otherpattern'); + await PageObjects.lens.switchDataPanelIndexPattern('log*'); await browser.refresh(); await PageObjects.header.waitUntilLoadingHasFinished(); - expect(await PageObjects.lens.getDataPanelIndexPattern()).to.equal('otherpattern'); + expect(await PageObjects.lens.getDataPanelIndexPattern()).to.equal('log*'); }); it('keeps time range and pinned filters after refreshing directly after saving', async () => { diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 2375a8acb6442e..29b42230673c91 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -327,9 +327,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should allow to change index pattern', async () => { - await PageObjects.lens.switchFirstLayerIndexPattern('otherpattern'); - expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal('otherpattern'); - expect(await PageObjects.lens.isShowingNoResults()).to.equal(true); + await PageObjects.lens.switchFirstLayerIndexPattern('log*'); + expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal('log*'); }); }); } 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 10754d20118e9b..d612a3776d2115 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/es_archives/lens/basic/data.json.gz b/x-pack/test/functional/es_archives/lens/basic/data.json.gz index c9ae08fe6f6281..5e43aa490e1dbc 100644 Binary files a/x-pack/test/functional/es_archives/lens/basic/data.json.gz and b/x-pack/test/functional/es_archives/lens/basic/data.json.gz differ 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 79e8c14cc39827..71b4a85d63f089 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/rule_exceptions/README.md b/x-pack/test/functional/es_archives/rule_exceptions/README.md new file mode 100644 index 00000000000000..1fbf4962d55fea --- /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 00000000000000..dd1609070a19d6 --- /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 00000000000000..28c0158cdc8523 --- /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 00000000000000..1f7a5969f5872a --- /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 00000000000000..bd69ae19ed1480 --- /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 00000000000000..2bdd685fae4c9b --- /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 00000000000000..a3b3fc52325a5c --- /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 00000000000000..888be5ff20a32f --- /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 00000000000000..b0a7b1a7fc60c8 --- /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 00000000000000..4d8575d3ccb9c3 --- /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 00000000000000..7e66ace5eb5c6b --- /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 00000000000000..5e2f1295397e6b --- /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 00000000000000..a05f3ec4e31866 --- /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 00000000000000..5d0ac56e27d001 --- /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 00000000000000..e98d0d89214dd0 --- /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 00000000000000..5dde1cba8f8849 --- /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 00000000000000..ceb58bc9335079 --- /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 00000000000000..09c54843f32c98 --- /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 00000000000000..bc8becbe07f451 --- /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 00000000000000..807314bd28173b --- /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 00000000000000..75b156805af785 --- /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 00000000000000..3604026d2cdb0d --- /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 00000000000000..8fe9af08127d1e --- /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 00000000000000..8d3da48224cc3b --- /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 00000000000000..5d3304fc202d54 --- /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 00000000000000..a0caf9d9eb2d3f --- /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 00000000000000..b981af79371241 --- /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 00000000000000..40dd24f83c0d24 --- /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 00000000000000..1b6a697ecbb8fc --- /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 00000000000000..4b147a414f8b3d --- /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 2d9ee00234bb67..ef80ab475cbd67 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 6d8eade25d7e6d..1aa6216236827d 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/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 7b7a6173fb4081..ae9814e603b743 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 5870239b73ed11..224048e868d7f0 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 11af83631502bd..95f3770443ccbb 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 4fb0511db21942..94e5e6faa2b431 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 5f827dd3eded64..c246e2945a6dd9 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/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 index 8804c2cd2ad598..2f0f124804626b 100644 --- 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 @@ -37,7 +37,6 @@ export default function ({ getService }: FtrProviderContext) { 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; 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 00000000000000..1742bd09c92f5a --- /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 9688d42cb43617..97c7b4334c3b7b 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 00000000000000..3819d26ae5efa5 --- /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 00000000000000..e7c876f54ee5a0 --- /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 cf141972b044a1..edcc1b5744fe37 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 9fc4c54ba13444..2ee47491c5ff38 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 00000000000000..8c208625590927 --- /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 153387c52e5c3c..a08fae4cdb0a13 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 0d1060fbf1f513..ee25e365d495d3 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/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 f471b83fbbc6b4..c63d85bd82dc01 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -172,6 +172,12 @@ export interface AggregationOptionsByType { field?: string; background_filter?: Record<string, any>; } & AggregationSourceOptions; + bucket_selector: { + buckets_path: { + [x: string]: string; + }; + script: string; + }; } type AggregationType = keyof AggregationOptionsByType; @@ -204,14 +210,14 @@ type SubAggregationResponseOf< interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOptionsMap, TDocument> { terms: { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; buckets: Array< { doc_count: number; key: string | number; } & SubAggregationResponseOf<TAggregationOptionsMap['aggs'], TDocument> >; + doc_count_error_upper_bound?: number; + sum_other_doc_count?: number; }; histogram: { buckets: Array< @@ -362,6 +368,7 @@ interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOpti >; }; bucket_sort: undefined; + bucket_selector: undefined; } // Type for debugging purposes. If you see an error in AggregationResponseMap @@ -379,11 +386,9 @@ interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOpti // Union keys are not included in keyof. The type will fall back to keyof T if // UnionToIntersection fails, which happens when there are conflicts between the union // types, e.g. { foo: string; bar?: undefined } | { foo?: undefined; bar: string }; -export type ValidAggregationKeysOf<T extends Record<string, any>> = keyof (UnionToIntersection< - T -> extends never - ? T - : UnionToIntersection<T>); +export type ValidAggregationKeysOf< + T extends Record<string, any> +> = keyof (UnionToIntersection<T> extends never ? T : UnionToIntersection<T>); export type AggregationResultOf< TAggregationOptionsMap extends AggregationOptionsMap, diff --git a/x-pack/typings/elasticsearch/index.d.ts b/x-pack/typings/elasticsearch/index.d.ts index f5d595edcd71e2..f2c51f601a099a 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -5,7 +5,7 @@ */ import { SearchParams, SearchResponse } from 'elasticsearch'; -import { AggregationResponseMap, AggregationInputMap } from './aggregations'; +import { AggregationResponseMap, AggregationInputMap, SortOptions } from './aggregations'; export { AggregationInputMap, AggregationOptionsByType, @@ -23,7 +23,13 @@ interface CollapseQuery { inner_hits: { name: string; size?: number; - sort?: [{ date: 'asc' | 'desc' }]; + sort?: SortOptions; + _source?: { + includes: string[]; + }; + collapse?: { + field: string; + }; }; max_concurrent_group_searches?: number; } @@ -31,9 +37,11 @@ interface CollapseQuery { export interface ESSearchBody { query?: any; size?: number; + from?: number; aggs?: AggregationInputMap; track_total_hits?: boolean | number; collapse?: CollapseQuery; + _source?: string | string[] | { excludes: string | string[] }; } export type ESSearchRequest = Omit<SearchParams, 'body'> & { 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 fedb4ebf0ba127..355832d14e6011 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2657,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 "" @@ -4485,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" @@ -5374,11 +5373,16 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.16.1.tgz#328d1c9b54402e44119398bcb6a31b7bbd606d59" integrity sha512-db6pZL5QY3JrlCHBhYQzYDci0xnoDuxfseUuguLRr3JNk+bnCfpkK6p8quiUDyO8A0vbpBKkk59Fw125etrNeA== -"@types/prettier@^2.0.0", "@types/prettier@^2.0.2": +"@types/prettier@^2.0.0": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3" integrity sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA== +"@types/prettier@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" + integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== + "@types/pretty-ms@^5.0.0": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/pretty-ms/-/pretty-ms-5.0.1.tgz#f2f0d7be58caf8613d149053d446e0282ae11ff3" @@ -10034,15 +10038,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" @@ -11101,14 +11096,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" @@ -12636,10 +12623,10 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-config-prettier@^6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" - integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== +eslint-config-prettier@^6.15.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" + integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw== dependencies: get-stdin "^6.0.0" @@ -19760,7 +19747,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= @@ -22437,10 +22424,10 @@ prettier@1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== -prettier@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" - integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== +prettier@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.0.tgz#8a03c7777883b29b37fb2c4348c66a78e980418b" + integrity sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw== prettier@~2.0.5: version "2.0.5" @@ -22516,10 +22503,10 @@ pretty-ms@^4.0.0: dependencies: parse-ms "^2.0.0" -prismjs@^1.8.4, prismjs@~1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308" - integrity sha512-OA4MKxjFZHSvZcisLGe14THYsug/nF6O1f0pAJc0KN0wTyAcLqmsbE+lTGKSpyh+9pEW57+k6pg2AfYR+coyHA== +prismjs@1.22.0, prismjs@^1.8.4, prismjs@~1.16.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.22.0.tgz#73c3400afc58a823dd7eed023f8e1ce9fd8977fa" + integrity sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w== optionalDependencies: clipboard "^2.0.0"