diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage new file mode 100644 index 0000000000000..d9ec1861c9979 --- /dev/null +++ b/.ci/Jenkinsfile_coverage @@ -0,0 +1,112 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() // load from the Jenkins instance + +stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit + timeout(time: 180, unit: 'MINUTES') { + timestamps { + ansiColor('xterm') { + catchError { + withEnv([ + 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + ]) { + parallel([ + 'kibana-intake-agent': { + withEnv([ + 'NODE_ENV=test' // Needed for jest tests only + ]) { + kibanaPipeline.legacyJobRunner('kibana-intake')() + } + }, + 'x-pack-intake-agent': { + withEnv([ + 'NODE_ENV=test' // Needed for jest tests only + ]) { + kibanaPipeline.legacyJobRunner('x-pack-intake')() + } + }, + 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), + 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), + 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), + 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), + 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), + 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), + 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), + 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), + 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), + 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), + 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), + 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), + ]), + 'kibana-xpack-agent-1': kibanaPipeline.withWorkers('kibana-xpack-tests-1', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), + 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), + ]), + 'kibana-xpack-agent-2': kibanaPipeline.withWorkers('kibana-xpack-tests-2', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), + 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), + ]), + + 'kibana-xpack-agent-3': kibanaPipeline.withWorkers('kibana-xpack-tests-3', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), + 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), + 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), + 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), + 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), + 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), + ]), + ]) + kibanaPipeline.jobRunner('tests-l', false) { + kibanaPipeline.downloadCoverageArtifacts() + kibanaPipeline.bash( + ''' + # bootstrap from x-pack folder + source src/dev/ci_setup/setup_env.sh + cd x-pack + yarn kbn bootstrap --prefer-offline + cd .. + # extract archives + mkdir -p /tmp/extracted_coverage + echo extracting intakes + tar -xzf /tmp/downloaded_coverage/coverage/kibana-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage + tar -xzf /tmp/downloaded_coverage/coverage/x-pack-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage + echo extracting kibana-oss-tests + tar -xzf /tmp/downloaded_coverage/coverage/kibana-oss-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage + echo extracting kibana-xpack-tests + for i in {1..3}; do + tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests-${i}/kibana-coverage.tar.gz -C /tmp/extracted_coverage + done + # replace path in json files to have valid html report + pwd=$(pwd) + du -sh /tmp/extracted_coverage/target/kibana-coverage/ + echo replacing path in json files + for i in {1..9}; do + sed -i "s|/dev/shm/workspace/kibana|$pwd|g" /tmp/extracted_coverage/target/kibana-coverage/functional/${i}*.json & + done + wait + # merge oss & x-pack reports + echo merging coverage reports + yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/jest --report-dir target/kibana-coverage/jest-combined --reporter=html --reporter=json-summary + yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/functional --report-dir target/kibana-coverage/functional-combined --reporter=html --reporter=json-summary + echo copy mocha reports + mkdir -p target/kibana-coverage/mocha-combined + cp -r /tmp/extracted_coverage/target/kibana-coverage/mocha target/kibana-coverage/mocha-combined + ''', + "run `yarn kbn bootstrap && merge coverage`" + ) + sh 'tar -czf kibana-jest-coverage.tar.gz target/kibana-coverage/jest-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/jest-combined", 'kibana-jest-coverage.tar.gz') + sh 'tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/functional-combined", 'kibana-functional-coverage.tar.gz') + sh 'tar -czf kibana-mocha-coverage.tar.gz target/kibana-coverage/mocha-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/mocha-combined", 'kibana-mocha-coverage.tar.gz') + } + } + } + kibanaPipeline.sendMail() + } + } + } +} diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 669395564db44..f702405aad69e 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -99,7 +99,7 @@ def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWor def numberOfWorkers = Math.min(numberOfExecutions, maxWorkerProcesses) for(def i = 1; i <= numberOfWorkers; i++) { - def workerExecutions = numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0) + def workerExecutions = floor(numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0)) workerMap["agent-${agentNumber}-worker-${i}"] = { workerNumber -> for(def j = 0; j < workerExecutions; j++) { diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es new file mode 100644 index 0000000000000..ad0ad54275e12 --- /dev/null +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -0,0 +1,162 @@ +#!/bin/groovy + +// This job effectively has two SCM configurations: +// one for kibana, used to check out this Jenkinsfile (which means it's the job's main SCM configuration), as well as kick-off the downstream verification job +// one for elasticsearch, used to check out the elasticsearch source before building it + +// There are two parameters that drive which branch is checked out for each of these, but they will typically be the same +// 'branch_specifier' is for kibana / the job itself +// ES_BRANCH is for elasticsearch + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +def ES_BRANCH = params.ES_BRANCH + +if (!ES_BRANCH) { + error "Parameter 'ES_BRANCH' must be specified." +} + +currentBuild.displayName += " - ${ES_BRANCH}" +currentBuild.description = "ES: ${ES_BRANCH}
Kibana: ${params.branch_specifier}" + +def PROMOTE_WITHOUT_VERIFY = !!params.PROMOTE_WITHOUT_VERIFICATION + +timeout(time: 120, unit: 'MINUTES') { + timestamps { + ansiColor('xterm') { + node('linux && immutable') { + catchError { + def VERSION + def SNAPSHOT_ID + def DESTINATION + + def scmVars = checkoutEs(ES_BRANCH) + def GIT_COMMIT = scmVars.GIT_COMMIT + def GIT_COMMIT_SHORT = sh(script: "git rev-parse --short ${GIT_COMMIT}", returnStdout: true).trim() + + buildArchives('to-archive') + + dir('to-archive') { + def now = new Date() + def date = now.format("yyyyMMdd-HHmmss") + + def filesRaw = sh(script: "ls -1", returnStdout: true).trim() + def files = filesRaw + .split("\n") + .collect { filename -> + // Filename examples + // elasticsearch-oss-8.0.0-SNAPSHOT-linux-x86_64.tar.gz + // elasticsearch-8.0.0-SNAPSHOT-linux-x86_64.tar.gz + def parts = filename.replace("elasticsearch-oss", "oss").split("-") + + VERSION = VERSION ?: parts[1] + SNAPSHOT_ID = SNAPSHOT_ID ?: "${date}_${GIT_COMMIT_SHORT}" + DESTINATION = DESTINATION ?: "${VERSION}/archives/${SNAPSHOT_ID}" + + return [ + filename: filename, + checksum: filename + '.sha512', + url: "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${DESTINATION}/${filename}".toString(), + version: parts[1], + platform: parts[3], + architecture: parts[4].split('\\.')[0], + license: parts[0] == 'oss' ? 'oss' : 'default', + ] + } + + sh 'find * -exec bash -c "shasum -a 512 {} > {}.sha512" \\;' + + def manifest = [ + bucket: "kibana-ci-es-snapshots-daily/${DESTINATION}".toString(), + branch: ES_BRANCH, + sha: GIT_COMMIT, + sha_short: GIT_COMMIT_SHORT, + version: VERSION, + generated: now.format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")), + archives: files, + ] + def manifestJson = toJSON(manifest).toString() + writeFile file: 'manifest.json', text: manifestJson + + upload(DESTINATION, '*.*') + + sh "cp manifest.json manifest-latest.json" + upload(VERSION, 'manifest-latest.json') + } + + if (PROMOTE_WITHOUT_VERIFY) { + esSnapshots.promote(VERSION, SNAPSHOT_ID) + + emailext( + to: 'build-kibana@elastic.co', + subject: "ES snapshot promoted without verification: ${params.ES_BRANCH}", + body: '${SCRIPT,template="groovy-html.template"}', + mimeType: 'text/html', + ) + } else { + build( + propagate: false, + wait: false, + job: 'elasticsearch+snapshots+verify', + parameters: [ + string(name: 'branch_specifier', value: branch_specifier), + string(name: 'SNAPSHOT_VERSION', value: VERSION), + string(name: 'SNAPSHOT_ID', value: SNAPSHOT_ID), + ] + ) + } + } + + kibanaPipeline.sendMail() + } + } + } +} + +def checkoutEs(branch) { + retryWithDelay(8, 15) { + return checkout([ + $class: 'GitSCM', + branches: [[name: branch]], + doGenerateSubmoduleConfigurations: false, + extensions: [], + submoduleCfg: [], + userRemoteConfigs: [[ + credentialsId: 'f6c7695a-671e-4f4f-a331-acdce44ff9ba', + url: 'git@github.com:elastic/elasticsearch', + ]], + ]) + } +} + +def upload(destination, pattern) { + return googleStorageUpload( + credentialsId: 'kibana-ci-gcs-plugin', + bucket: "gs://kibana-ci-es-snapshots-daily/${destination}", + pattern: pattern, + sharedPublicly: false, + showInline: false, + ) +} + +def buildArchives(destination) { + def props = readProperties file: '.ci/java-versions.properties' + withEnv([ + // Select the correct JDK for this branch + "PATH=/var/lib/jenkins/.java/${props.ES_BUILD_JAVA}/bin:${env.PATH}", + + // These Jenkins env vars trigger some automation in the elasticsearch repo that we don't want + "BUILD_NUMBER=", + "JENKINS_URL=", + "BUILD_URL=", + "JOB_NAME=", + "NODE_NAME=", + ]) { + sh """ + ./gradlew -p distribution/archives assemble --parallel + mkdir -p ${destination} + find distribution/archives -type f \\( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \\) -not -path *no-jdk* -exec cp {} ${destination} \\; + """ + } +} diff --git a/.ci/es-snapshots/Jenkinsfile_trigger_build_es b/.ci/es-snapshots/Jenkinsfile_trigger_build_es new file mode 100644 index 0000000000000..186917e967824 --- /dev/null +++ b/.ci/es-snapshots/Jenkinsfile_trigger_build_es @@ -0,0 +1,19 @@ +#!/bin/groovy + +if (!params.branches_yaml) { + error "'branches_yaml' parameter must be specified" +} + +def branches = readYaml text: params.branches_yaml + +branches.each { branch -> + build( + propagate: false, + wait: false, + job: 'elasticsearch+snapshots+build', + parameters: [ + string(name: 'branch_specifier', value: branch), + string(name: 'ES_BRANCH', value: branch), + ] + ) +} diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es new file mode 100644 index 0000000000000..3d5ec75fa0e72 --- /dev/null +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -0,0 +1,72 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +def SNAPSHOT_VERSION = params.SNAPSHOT_VERSION +def SNAPSHOT_ID = params.SNAPSHOT_ID + +if (!SNAPSHOT_VERSION) { + error "Parameter SNAPSHOT_VERSION must be specified" +} + +if (!SNAPSHOT_ID) { + error "Parameter SNAPSHOT_ID must be specified" +} + +currentBuild.displayName += " - ${SNAPSHOT_VERSION}" +currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch_specifier}" + +def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" + +timeout(time: 120, unit: 'MINUTES') { + timestamps { + ansiColor('xterm') { + catchError { + withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { + parallel([ + // TODO we just need to run integration tests from intake? + 'kibana-intake-agent': kibanaPipeline.legacyJobRunner('kibana-intake'), + 'x-pack-intake-agent': kibanaPipeline.legacyJobRunner('x-pack-intake'), + 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), + 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), + 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), + 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), + 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), + 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), + 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), + 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), + 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), + 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), + 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), + 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), + ]), + 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), + 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), + 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), + 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), + 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), + 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), + 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), + 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), + 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), + 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), + ]), + ]) + } + + promoteSnapshot(SNAPSHOT_VERSION, SNAPSHOT_ID) + } + + kibanaPipeline.sendMail() + } + } +} + +def promoteSnapshot(snapshotVersion, snapshotId) { + node('linux && immutable') { + esSnapshots.promote(snapshotVersion, snapshotId) + } +} diff --git a/.eslintrc.js b/.eslintrc.js index 03a674993ab50..a7bb204da4775 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -82,43 +82,12 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['src/legacy/core_plugins/kibana/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/rules-of-hooks': 'off', - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: ['src/legacy/core_plugins/tile_map/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: ['src/legacy/core_plugins/vis_type_markdown/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: ['src/legacy/core_plugins/vis_type_metric/**/*.{js,ts,tsx}'], - rules: { - 'jsx-a11y/click-events-have-key-events': 'off', - }, - }, { files: ['src/legacy/core_plugins/vis_type_table/**/*.{js,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['src/legacy/core_plugins/vis_type_vega/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/legacy/ui/public/vis/**/*.{js,ts,tsx}'], rules: { @@ -177,12 +146,6 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['x-pack/legacy/plugins/monitoring/**/*.{js,ts,tsx}'], - rules: { - 'jsx-a11y/click-events-have-key-events': 'off', - }, - }, { files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,ts,tsx}'], rules: { @@ -253,6 +216,7 @@ module.exports = { '!x-pack/test/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'src/core/(public|server)/**/*', + 'examples/**/*', ], from: [ 'src/core/public/**/*', @@ -289,11 +253,15 @@ module.exports = { 'x-pack/legacy/plugins/**/*', '!x-pack/legacy/plugins/*/server/**/*', '!x-pack/legacy/plugins/*/index.{js,ts,tsx}', + + 'examples/**/*', + '!examples/**/server/**/*', ], from: [ 'src/core/server', 'src/core/server/**/*', '(src|x-pack)/plugins/*/server/**/*', + 'examples/**/server/**/*', ], errorMessage: 'Server modules cannot be imported into client modules or shared modules.', @@ -373,9 +341,8 @@ module.exports = { 'src/fixtures/**/*.js', // TODO: this directory needs to be more obviously "public" (or go away) ], settings: { - // instructs import/no-extraneous-dependencies to treat modules - // in plugins/ or ui/ namespace as "core modules" so they don't - // trigger failures for not being listed in package.json + // instructs import/no-extraneous-dependencies to treat certain modules + // as core modules, even if they aren't listed in package.json 'import/core-modules': [ 'plugins', 'legacy/ui', @@ -729,15 +696,13 @@ module.exports = { 'no-unreachable': 'error', 'no-unsafe-finally': 'error', 'no-useless-call': 'error', - // This will be turned on after bug fixes are mostly complete - // 'no-useless-catch': 'warn', + 'no-useless-catch': 'error', 'no-useless-concat': 'error', 'no-useless-computed-key': 'error', // This will be turned on after bug fixes are mostly complete // 'no-useless-escape': 'warn', 'no-useless-rename': 'error', - // This will be turned on after bug fixes are mostly complete - // 'no-useless-return': 'warn', + 'no-useless-return': 'error', // This will be turned on after bug fixers are mostly complete // 'no-void': 'warn', 'one-var-declaration-per-line': 'error', @@ -745,14 +710,13 @@ module.exports = { 'prefer-promise-reject-errors': 'error', 'prefer-rest-params': 'error', 'prefer-spread': 'error', - // This style will be turned on after most bugs are fixed - // 'prefer-template': 'warn', + 'prefer-template': 'error', 'react/boolean-prop-naming': 'error', 'react/button-has-type': 'error', + 'react/display-name': 'error', 'react/forbid-dom-props': 'error', 'react/no-access-state-in-setstate': 'error', - // This style will be turned on after most bugs are fixed - // 'react/no-children-prop': 'warn', + 'react/no-children-prop': 'error', 'react/no-danger-with-children': 'error', 'react/no-deprecated': 'error', 'react/no-did-mount-set-state': 'error', @@ -814,21 +778,6 @@ module.exports = { }, }, - /** - * Monitoring overrides - */ - { - files: ['x-pack/legacy/plugins/monitoring/**/*.js'], - rules: { - 'no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }], - 'no-else-return': 'error', - }, - }, - { - files: ['x-pack/legacy/plugins/monitoring/public/**/*.js'], - env: { browser: true }, - }, - /** * Canvas overrides */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1137fb99f81a7..9a4f2b71da1ff 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,7 @@ /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/home/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app +/src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/plugins/home/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app @@ -30,6 +31,7 @@ /src/plugins/visualizations/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch /src/legacy/core_plugins/data/ @elastic/kibana-app-arch +/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch @@ -84,6 +86,7 @@ /packages/kbn-es/ @elastic/kibana-operations /packages/kbn-pm/ @elastic/kibana-operations /packages/kbn-test/ @elastic/kibana-operations +/packages/kbn-ui-shared-deps/ @elastic/kibana-operations /src/legacy/server/keystore/ @elastic/kibana-operations /src/legacy/server/pid/ @elastic/kibana-operations /src/legacy/server/sass/ @elastic/kibana-operations @@ -146,6 +149,3 @@ /x-pack/legacy/plugins/searchprofiler/ @elastic/es-ui /x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui /x-pack/legacy/plugins/watcher/ @elastic/es-ui - -# Kibana TSVB external contractors -/src/legacy/core_plugins/metrics/ @elastic/kibana-tsvb-external diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06e08c85dafec..6ae3db559b61b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,6 +190,19 @@ These snapshots are built on a nightly basis which expire after a couple weeks. yarn es snapshot ``` +##### Keeping data between snapshots + +If you want to keep the data inside your Elasticsearch between usages of this command, +you should use the following command, to keep your data folder outside the downloaded snapshot +folder: + +```bash +yarn es snapshot -E path.data=../data +``` + +The same parameter can be used with the source and archive command shown in the following +paragraphs. + #### Source By default, it will reference an [elasticsearch](https://github.com/elastic/elasticsearch) checkout which is a sibling to the Kibana directory named `elasticsearch`. If you wish to use a checkout in another location you can provide that by supplying `--source-path` diff --git a/docs/apm/settings.asciidoc b/docs/apm/settings.asciidoc index 2fc8748f13b09..37122fc9c635d 100644 --- a/docs/apm/settings.asciidoc +++ b/docs/apm/settings.asciidoc @@ -3,8 +3,16 @@ [[apm-settings-in-kibana]] === APM settings in Kibana -You do not need to configure any settings to use APM. It is enabled by default. -If you'd like to change any of the default values, -copy and paste the relevant settings below into your `kibana.yml` configuration file. +You do not need to configure any settings to use the APM app. It is enabled by default. + +[float] +[[apm-indices-settings]] +==== APM Indices + +include::./../settings/apm-settings.asciidoc[tag=apm-indices-settings] + +[float] +[[general-apm-settings]] +==== General APM settings include::./../settings/apm-settings.asciidoc[tag=general-apm-settings] diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index ec0863b09d653..22279b69b70fe 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -17,6 +17,7 @@ This section can help with any of the following: There are a number of factors that could be at play here. One important thing to double-check first is your index template. +*Index template* An APM index template must exist for the APM app to work correctly. By default, this index template is created by APM Server on startup. However, this only happens if `setup.template.enabled` is `true` in `apm-server.yml`. @@ -34,14 +35,21 @@ GET /_template/apm-{version} -------------------------------------------------- // CONSOLE +*Using Logstash, Kafka, etc.* If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka), then the index template will not be set up automatically. Instead, you'll need to -{apm-server-ref}/_manually_loading_template_configuration.html#load-template-manually-alternate[load the template manually]. +{apm-server-ref}/_manually_loading_template_configuration.html[load the template manually]. -Finally, this problem can also occur if you've changed the index name that you write APM data to. -The default index pattern can be found {apm-server-ref}/elasticsearch-output.html#index-option-es[here]. -If you change this setting, you must also configure the `setup.template.name` and `setup.template.pattern` options. +*Using a custom index names* +This problem can also occur if you've customized the index name that you write APM data to. +The default index name that APM writes events to can be found +{apm-server-ref}/elasticsearch-output.html#index-option-es[here]. +If you change the default, you must also configure the `setup.template.name` and `setup.template.pattern` options. See {apm-server-ref}/configuration-template.html[Load the Elasticsearch index template]. +If the Elasticsearch index template has already been successfully loaded to the index, +you can customize the indices that the APM app uses to display data. +Navigate to *APM* > *Settings* > *Indices*, and change all `apm_oss.*Pattern` values to +include the new index pattern. For example: `customIndexName-*`. ==== Unknown route diff --git a/docs/developer/plugin/development-uiexports.asciidoc b/docs/developer/plugin/development-uiexports.asciidoc index 6368446f7fb43..18d326cbfb9c0 100644 --- a/docs/developer/plugin/development-uiexports.asciidoc +++ b/docs/developer/plugin/development-uiexports.asciidoc @@ -9,8 +9,8 @@ An aggregate list of available UiExport types: | hacks | Any module that should be included in every application | visTypes | Modules that register providers with the `ui/registry/vis_types` registry. | inspectorViews | Modules that register custom inspector views via the `viewRegistry` in `ui/inspector`. -| chromeNavControls | Modules that register providers with the `ui/registry/chrome_nav_controls` registry. -| navbarExtensions | Modules that register providers with the `ui/registry/navbar_extensions` registry. -| docViews | Modules that register providers with the `ui/registry/doc_views` registry. +| chromeNavControls | Modules that register providers with the `ui/registry/chrome_header_nav_controls` registry. +| navbarExtensions | Modules that register providers with the setup contract of the `navigation` plugin. +| docViews | Modules that register providers with the setup contract method `addDocView` of the `discover` plugin. | app | Adds an application to the system. This uiExport type is defined as an object of metadata rather than just a module id. |======================================================================= diff --git a/docs/development/core/public/kibana-plugin-public.appbase.chromeless.md b/docs/development/core/public/kibana-plugin-public.appbase.chromeless.md new file mode 100644 index 0000000000000..ddbf9aafbd28a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.chromeless.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [chromeless](./kibana-plugin-public.appbase.chromeless.md) + +## AppBase.chromeless property + +Hide the UI chrome when the application is mounted. Defaults to `false`. Takes precedence over chrome service visibility settings. + +Signature: + +```typescript +chromeless?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.id.md b/docs/development/core/public/kibana-plugin-public.appbase.id.md index 57daa0c94bdf6..89dd32d296104 100644 --- a/docs/development/core/public/kibana-plugin-public.appbase.id.md +++ b/docs/development/core/public/kibana-plugin-public.appbase.id.md @@ -4,6 +4,8 @@ ## AppBase.id property +The unique identifier of the application + Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-public.appbase.md b/docs/development/core/public/kibana-plugin-public.appbase.md index a93a195c559b1..eb6d91cb92488 100644 --- a/docs/development/core/public/kibana-plugin-public.appbase.md +++ b/docs/development/core/public/kibana-plugin-public.appbase.md @@ -16,10 +16,14 @@ export interface AppBase | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | +| [chromeless](./kibana-plugin-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | | [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [icon](./kibana-plugin-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | -| [id](./kibana-plugin-public.appbase.id.md) | string | | +| [id](./kibana-plugin-public.appbase.id.md) | string | The unique identifier of the application | +| [navLinkStatus](./kibana-plugin-public.appbase.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) | | [order](./kibana-plugin-public.appbase.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [status](./kibana-plugin-public.appbase.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | | [title](./kibana-plugin-public.appbase.title.md) | string | The title of the application. | -| [tooltip$](./kibana-plugin-public.appbase.tooltip_.md) | Observable<string> | An observable for a tooltip shown when hovering over app link. | +| [tooltip](./kibana-plugin-public.appbase.tooltip.md) | string | A tooltip shown when hovering over app link. | +| [updater$](./kibana-plugin-public.appbase.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) at runtime. | diff --git a/docs/development/core/public/kibana-plugin-public.appbase.navlinkstatus.md b/docs/development/core/public/kibana-plugin-public.appbase.navlinkstatus.md new file mode 100644 index 0000000000000..d6744c3e75756 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.navlinkstatus.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [navLinkStatus](./kibana-plugin-public.appbase.navlinkstatus.md) + +## AppBase.navLinkStatus property + +The initial status of the application's navLink. Defaulting to `visible` if `status` is `accessible` and `hidden` if status is `inaccessible` See [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) + +Signature: + +```typescript +navLinkStatus?: AppNavLinkStatus; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.tooltip_.md b/docs/development/core/public/kibana-plugin-public.appbase.status.md similarity index 56% rename from docs/development/core/public/kibana-plugin-public.appbase.tooltip_.md rename to docs/development/core/public/kibana-plugin-public.appbase.status.md index 0767ead5f1455..a5fbadbeea1ff 100644 --- a/docs/development/core/public/kibana-plugin-public.appbase.tooltip_.md +++ b/docs/development/core/public/kibana-plugin-public.appbase.status.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [tooltip$](./kibana-plugin-public.appbase.tooltip_.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [status](./kibana-plugin-public.appbase.status.md) -## AppBase.tooltip$ property +## AppBase.status property -An observable for a tooltip shown when hovering over app link. +The initial status of the application. Defaulting to `accessible` Signature: ```typescript -tooltip$?: Observable; +status?: AppStatus; ``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.tooltip.md b/docs/development/core/public/kibana-plugin-public.appbase.tooltip.md new file mode 100644 index 0000000000000..85921a5a321dd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.tooltip.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [tooltip](./kibana-plugin-public.appbase.tooltip.md) + +## AppBase.tooltip property + +A tooltip shown when hovering over app link. + +Signature: + +```typescript +tooltip?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.updater_.md b/docs/development/core/public/kibana-plugin-public.appbase.updater_.md new file mode 100644 index 0000000000000..3edd357383449 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.updater_.md @@ -0,0 +1,44 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [updater$](./kibana-plugin-public.appbase.updater_.md) + +## AppBase.updater$ property + +An [AppUpdater](./kibana-plugin-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) at runtime. + +Signature: + +```typescript +updater$?: Observable; +``` + +## Example + +How to update an application navLink at runtime + +```ts +// inside your plugin's setup function +export class MyPlugin implements Plugin { + private appUpdater = new BehaviorSubject(() => ({})); + + setup({ application }) { + application.register({ + id: 'my-app', + title: 'My App', + updater$: this.appUpdater, + async mount(params) { + const { renderApp } = await import('./application'); + return renderApp(params); + }, + }); + } + + start() { + // later, when the navlink needs to be updated + appUpdater.next(() => { + navLinkStatus: AppNavLinkStatus.disabled, + }) + } + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.appleaveaction.md b/docs/development/core/public/kibana-plugin-public.appleaveaction.md new file mode 100644 index 0000000000000..ae56205f5e45c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleaveaction.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) + +## AppLeaveAction type + +Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) + +See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) + +Signature: + +```typescript +export declare type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appleaveactiontype.md b/docs/development/core/public/kibana-plugin-public.appleaveactiontype.md new file mode 100644 index 0000000000000..482b2e489b33e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleaveactiontype.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) + +## AppLeaveActionType enum + +Possible type of actions on application leave. + +Signature: + +```typescript +export declare enum AppLeaveActionType +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| confirm | "confirm" | | +| default | "default" | | + diff --git a/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.md b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.md new file mode 100644 index 0000000000000..4cd99dd2a3fb3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) + +## AppLeaveConfirmAction interface + +Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application. + +See + +Signature: + +```typescript +export interface AppLeaveConfirmAction +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [text](./kibana-plugin-public.appleaveconfirmaction.text.md) | string | | +| [title](./kibana-plugin-public.appleaveconfirmaction.title.md) | string | | +| [type](./kibana-plugin-public.appleaveconfirmaction.type.md) | AppLeaveActionType.confirm | | + diff --git a/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.text.md b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.text.md new file mode 100644 index 0000000000000..dbbd11c6f71f8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.text.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) > [text](./kibana-plugin-public.appleaveconfirmaction.text.md) + +## AppLeaveConfirmAction.text property + +Signature: + +```typescript +text: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.title.md b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.title.md new file mode 100644 index 0000000000000..684ef384a37c3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) > [title](./kibana-plugin-public.appleaveconfirmaction.title.md) + +## AppLeaveConfirmAction.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.type.md b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.type.md new file mode 100644 index 0000000000000..926cecf893cc8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleaveconfirmaction.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) > [type](./kibana-plugin-public.appleaveconfirmaction.type.md) + +## AppLeaveConfirmAction.type property + +Signature: + +```typescript +type: AppLeaveActionType.confirm; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appleavedefaultaction.md b/docs/development/core/public/kibana-plugin-public.appleavedefaultaction.md new file mode 100644 index 0000000000000..ed2f729a0c648 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleavedefaultaction.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) + +## AppLeaveDefaultAction interface + +Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application. + +See + +Signature: + +```typescript +export interface AppLeaveDefaultAction +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-public.appleavedefaultaction.type.md) | AppLeaveActionType.default | | + diff --git a/docs/development/core/public/kibana-plugin-public.appleavedefaultaction.type.md b/docs/development/core/public/kibana-plugin-public.appleavedefaultaction.type.md new file mode 100644 index 0000000000000..ee12393121a5a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleavedefaultaction.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) > [type](./kibana-plugin-public.appleavedefaultaction.type.md) + +## AppLeaveDefaultAction.type property + +Signature: + +```typescript +type: AppLeaveActionType.default; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appleavehandler.md b/docs/development/core/public/kibana-plugin-public.appleavehandler.md new file mode 100644 index 0000000000000..e879227961bc6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appleavehandler.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) + +## AppLeaveHandler type + +A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return `confirm` to to prompt a message to the user before leaving the page, or `default` to keep the default behavior (doing nothing). + +See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. + +Signature: + +```typescript +export declare type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md index a63de399c2ecb..cf9bc5189af40 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md @@ -16,5 +16,6 @@ export interface ApplicationSetup | Method | Description | | --- | --- | | [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. | +| [registerAppUpdater(appUpdater$)](./kibana-plugin-public.applicationsetup.registerappupdater.md) | Register an application updater that can be used to change the [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) fields of all applications at runtime.This is meant to be used by plugins that needs to updates the whole list of applications. To only updates a specific application, use the updater$ property of the registered application instead. | | [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerappupdater.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registerappupdater.md new file mode 100644 index 0000000000000..39b4f878a3f79 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registerappupdater.md @@ -0,0 +1,47 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerAppUpdater](./kibana-plugin-public.applicationsetup.registerappupdater.md) + +## ApplicationSetup.registerAppUpdater() method + +Register an application updater that can be used to change the [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) fields of all applications at runtime. + +This is meant to be used by plugins that needs to updates the whole list of applications. To only updates a specific application, use the `updater$` property of the registered application instead. + +Signature: + +```typescript +registerAppUpdater(appUpdater$: Observable): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appUpdater$ | Observable<AppUpdater> | | + +Returns: + +`void` + +## Example + +How to register an application updater that disables some applications: + +```ts +// inside your plugin's setup function +export class MyPlugin implements Plugin { + setup({ application }) { + application.registerAppUpdater( + new BehaviorSubject(app => { + if (myPluginApi.shouldDisable(app)) + return { + status: AppStatus.inaccessible, + }; + }) + ); + } +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 4baa4565ff7b0..e36ef3f14f87e 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -22,6 +22,6 @@ export interface ApplicationStart | Method | Description | | --- | --- | | [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | -| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app | +| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigate to a given app | | [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md index eef31fe661f54..3e29d09ea4cd5 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md @@ -4,7 +4,7 @@ ## ApplicationStart.navigateToApp() method -Navigiate to a given app +Navigate to a given app Signature: @@ -12,7 +12,7 @@ Navigiate to a given app navigateToApp(appId: string, options?: { path?: string; state?: any; - }): void; + }): Promise; ``` ## Parameters @@ -24,5 +24,5 @@ navigateToApp(appId: string, options?: { Returns: -`void` +`Promise` diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.md index aa5ca93ed8ff0..9586eba96a697 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.md @@ -17,4 +17,5 @@ export interface AppMountParameters | --- | --- | --- | | [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) | string | The route path for configuring navigation to the application. This string should not include the base path from HTTP. | | [element](./kibana-plugin-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | +| [onAppLeave](./kibana-plugin-public.appmountparameters.onappleave.md) | (handler: AppLeaveHandler) => void | A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. | diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md new file mode 100644 index 0000000000000..55eb840ce1cf6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [onAppLeave](./kibana-plugin-public.appmountparameters.onappleave.md) + +## AppMountParameters.onAppLeave property + +A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page. + +This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. + +Signature: + +```typescript +onAppLeave: (handler: AppLeaveHandler) => void; +``` + +## Example + + +```ts +// application.tsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; + +import { CoreStart, AppMountParams } from 'src/core/public'; +import { MyPluginDepsStart } from './plugin'; + +export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { + const { renderApp, hasUnsavedChanges } = await import('./application'); + onAppLeave(actions => { + if(hasUnsavedChanges()) { + return actions.confirm('Some changes were not saved. Are you sure you want to leave?'); + } + return actions.default(); + }); + return renderApp(params); +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.appnavlinkstatus.md b/docs/development/core/public/kibana-plugin-public.appnavlinkstatus.md new file mode 100644 index 0000000000000..d6b22ac2b9217 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appnavlinkstatus.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) + +## AppNavLinkStatus enum + +Status of the application's navLink. + +Signature: + +```typescript +export declare enum AppNavLinkStatus +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| default | 0 | The application navLink will be visible if the application's [AppStatus](./kibana-plugin-public.appstatus.md) is set to accessible and hidden if the application status is set to inaccessible. | +| disabled | 2 | The application navLink is visible but inactive and not clickable in the navigation bar. | +| hidden | 3 | The application navLink does not appear in the navigation bar. | +| visible | 1 | The application navLink is visible and clickable in the navigation bar. | + diff --git a/docs/development/core/public/kibana-plugin-public.appstatus.md b/docs/development/core/public/kibana-plugin-public.appstatus.md new file mode 100644 index 0000000000000..23fb7186569da --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appstatus.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppStatus](./kibana-plugin-public.appstatus.md) + +## AppStatus enum + +Accessibility status of an application. + +Signature: + +```typescript +export declare enum AppStatus +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| accessible | 0 | Application is accessible. | +| inaccessible | 1 | Application is not accessible. | + diff --git a/docs/development/core/public/kibana-plugin-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-public.appupdatablefields.md new file mode 100644 index 0000000000000..b9260c79cd972 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appupdatablefields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) + +## AppUpdatableFields type + +Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md). + +Signature: + +```typescript +export declare type AppUpdatableFields = Pick; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appupdater.md b/docs/development/core/public/kibana-plugin-public.appupdater.md new file mode 100644 index 0000000000000..f1b965cc2fc22 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appupdater.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppUpdater](./kibana-plugin-public.appupdater.md) + +## AppUpdater type + +Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) + +Signature: + +```typescript +export declare type AppUpdater = (app: AppBase) => Partial | undefined; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index 93ebbe3653ac4..4cb9080222ac5 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -24,7 +24,7 @@ export interface ChromeNavLink | [id](./kibana-plugin-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | | [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | | [order](./kibana-plugin-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an applcation. | +| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | | [title](./kibana-plugin-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | | [url](./kibana-plugin-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md index b9d12432a01df..1b8fb0574cf8b 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md @@ -8,7 +8,7 @@ > > -A url base that legacy apps can set to match deep URLs to an applcation. +A url base that legacy apps can set to match deep URLs to an application. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index e2c2866b57b6b..64cbdd880fed1 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -1,137 +1,151 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) - -## kibana-plugin-public package - -The Kibana Core APIs for client-side plugins. - -A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md). - -The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. - -## Classes - -| Class | Description | -| --- | --- | -| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. | -| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. | -| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. | - -## Interfaces - -| Interface | Description | -| --- | --- | -| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. | -| [AppBase](./kibana-plugin-public.appbase.md) | | -| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | -| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | -| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | -| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | | -| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | -| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | -| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | -| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. | -| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | -| [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | | -| [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | -| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | | -| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. | -| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. | -| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | | -| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. | -| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | -| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | -| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin start lifecycle | -| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | | -| [EnvironmentMode](./kibana-plugin-public.environmentmode.md) | | -| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. | -| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | -| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | -| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | | -| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | | -| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md). | -| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | | -| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. | -| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | -| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). | -| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. | -| [HttpSetup](./kibana-plugin-public.httpsetup.md) | | -| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | -| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | -| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | -| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | -| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | | -| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). | -| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | | -| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | -| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | -| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | -| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | -| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | -| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | -| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | -| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | | -| [OverlayRef](./kibana-plugin-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-public.overlaystart.md) methods for closing a mounted overlay. | -| [OverlayStart](./kibana-plugin-public.overlaystart.md) | | -| [PackageInfo](./kibana-plugin-public.packageinfo.md) | | -| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer. | -| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | -| [SavedObject](./kibana-plugin-public.savedobject.md) | | -| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | -| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. | -| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | | -| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | | -| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | | -| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | | -| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | | -| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | | -| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | | -| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | | -| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | -| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | -| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. | -| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | -| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. | -| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | -| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | -| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | -| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | -| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | -| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | | -| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | - -## Type Aliases - -| Type Alias | Description | -| --- | --- | -| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. | -| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | -| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | -| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | -| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | | -| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | | -| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | | -| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | | -| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | | -| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | -| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | -| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | -| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | -| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) | -| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | -| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). | -| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | -| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | -| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | | -| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | -| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | -| [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | -| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | -| [Toast](./kibana-plugin-public.toast.md) | | -| [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. | -| [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). | -| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) | -| [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) | -| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) | - + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) + +## kibana-plugin-public package + +The Kibana Core APIs for client-side plugins. + +A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md). + +The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. + +## Classes + +| Class | Description | +| --- | --- | +| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. | +| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. | +| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. | + +## Enumerations + +| Enumeration | Description | +| --- | --- | +| [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) | Possible type of actions on application leave. | +| [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) | Status of the application's navLink. | +| [AppStatus](./kibana-plugin-public.appstatus.md) | Accessibility status of an application. | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. | +| [AppBase](./kibana-plugin-public.appbase.md) | | +| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.See | +| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.See | +| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | +| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | +| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | +| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | | +| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | +| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | +| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | +| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. | +| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | +| [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | | +| [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | +| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | | +| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. | +| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. | +| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | | +| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. | +| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | +| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | +| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin start lifecycle | +| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | | +| [EnvironmentMode](./kibana-plugin-public.environmentmode.md) | | +| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. | +| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | +| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | +| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | | +| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | | +| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md). | +| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | | +| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. | +| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | +| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). | +| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. | +| [HttpSetup](./kibana-plugin-public.httpsetup.md) | | +| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | +| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | +| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | +| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | +| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | | +| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). | +| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | | +| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | +| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | +| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | +| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | +| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | +| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | +| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | +| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | | +| [OverlayRef](./kibana-plugin-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-public.overlaystart.md) methods for closing a mounted overlay. | +| [OverlayStart](./kibana-plugin-public.overlaystart.md) | | +| [PackageInfo](./kibana-plugin-public.packageinfo.md) | | +| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer. | +| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | +| [SavedObject](./kibana-plugin-public.savedobject.md) | | +| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | +| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | | +| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | | +| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | | +| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | | +| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | | +| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | | +| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | | +| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | | +| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | +| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | +| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. | +| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | +| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | +| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | +| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | | +| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | +| [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. | +| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. | +| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | +| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | +| [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md). | +| [AppUpdater](./kibana-plugin-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | +| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | +| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | | +| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | | +| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | | +| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | | +| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | | +| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | +| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | +| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | +| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | +| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) | +| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | +| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). | +| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | +| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | +| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | | +| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | +| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | +| [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | +| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | +| [Toast](./kibana-plugin-public.toast.md) | | +| [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. | +| [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). | +| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) | +| [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) | +| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) | + diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.md b/docs/development/core/public/kibana-plugin-public.overlaystart.md index 8b6f11bd819f8..a83044763344b 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.md @@ -16,6 +16,7 @@ export interface OverlayStart | Property | Type | Description | | --- | --- | --- | | [banners](./kibana-plugin-public.overlaystart.banners.md) | OverlayBannersStart | [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | +| [openConfirm](./kibana-plugin-public.overlaystart.openconfirm.md) | OverlayModalStart['openConfirm'] | | | [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | OverlayFlyoutStart['open'] | | | [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | OverlayModalStart['open'] | | diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.openconfirm.md b/docs/development/core/public/kibana-plugin-public.overlaystart.openconfirm.md new file mode 100644 index 0000000000000..543a69e0b3318 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.openconfirm.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openConfirm](./kibana-plugin-public.overlaystart.openconfirm.md) + +## OverlayStart.openConfirm property + + +Signature: + +```typescript +openConfirm: OverlayModalStart['openConfirm']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 1ce18834f5319..a4fa3f17d0d94 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 3b916db972673..88485aa71f7c5 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -24,7 +24,7 @@ The constructor for this class is marked as internal. Third-party code should no | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md index ed7d028a1ec8a..bb1f481c9ef4f 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md @@ -9,14 +9,14 @@ Creates an instance of [IScopedClusterClient](./kibana-plugin-server.iscopedclus Signature: ```typescript -asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): IScopedClusterClient; +asScoped(request?: ScopeableRequest): IScopedClusterClient; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| request | KibanaRequest | LegacyRequest | FakeRequest | Request the IScopedClusterClient instance will be scoped to. Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform | +| request | ScopeableRequest | Request the IScopedClusterClient instance will be scoped to. Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.md b/docs/development/core/server/kibana-plugin-server.clusterclient.md index 5fdda7ef3e499..d547b846e65b7 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.md @@ -4,7 +4,7 @@ ## ClusterClient class -Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [ClusterClient](./kibana-plugin-server.clusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient.md new file mode 100644 index 0000000000000..415423f555266 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient](./kibana-plugin-server.elasticsearchservicesetup.adminclient.md) + +## ElasticsearchServiceSetup.adminClient property + +A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). + +Signature: + +```typescript +readonly adminClient: IClusterClient; +``` + +## Example + + +```js +const client = core.elasticsearch.adminClient; + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md deleted file mode 100644 index b5bfc68d3ca0c..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) - -## ElasticsearchServiceSetup.adminClient$ property - -Observable of clients for the `admin` cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). - - -```js -const client = await elasticsearch.adminClient$.pipe(take(1)).toPromise(); - -``` - -Signature: - -```typescript -readonly adminClient$: Observable; -``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md index 3d26f2d4cec88..797f402cc2580 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md @@ -9,7 +9,7 @@ Create application specific Elasticsearch cluster API client with customized con Signature: ```typescript -readonly createClient: (type: string, clientConfig?: Partial) => IClusterClient; +readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient.md new file mode 100644 index 0000000000000..e9845dce6915d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient](./kibana-plugin-server.elasticsearchservicesetup.dataclient.md) + +## ElasticsearchServiceSetup.dataClient property + +A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). + +Signature: + +```typescript +readonly dataClient: IClusterClient; +``` + +## Example + + +```js +const client = core.elasticsearch.dataClient; + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md deleted file mode 100644 index 9411f2f6b8694..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) - -## ElasticsearchServiceSetup.dataClient$ property - -Observable of clients for the `data` cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). - - -```js -const client = await elasticsearch.dataClient$.pipe(take(1)).toPromise(); - -``` - -Signature: - -```typescript -readonly dataClient$: Observable; -``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md index e3d151cdc0d8b..2de3f6e6d1bbc 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md @@ -15,17 +15,7 @@ export interface ElasticsearchServiceSetup | Property | Type | Description | | --- | --- | --- | -| [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Observable<IClusterClient> | Observable of clients for the admin cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). -```js -const client = await elasticsearch.adminClient$.pipe(take(1)).toPromise(); - -``` - | -| [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => IClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). | -| [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Observable<IClusterClient> | Observable of clients for the data cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). -```js -const client = await elasticsearch.dataClient$.pipe(take(1)).toPromise(); - -``` - | +| [adminClient](./kibana-plugin-server.elasticsearchservicesetup.adminclient.md) | IClusterClient | A client for the admin cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). | +| [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). | +| [dataClient](./kibana-plugin-server.elasticsearchservicesetup.dataclient.md) | IClusterClient | A client for the data cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). | diff --git a/docs/development/core/server/kibana-plugin-server.iclusterclient.md b/docs/development/core/server/kibana-plugin-server.iclusterclient.md index 834afa6db5157..e7435a9d91a74 100644 --- a/docs/development/core/server/kibana-plugin-server.iclusterclient.md +++ b/docs/development/core/server/kibana-plugin-server.iclusterclient.md @@ -4,12 +4,12 @@ ## IClusterClient type -Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [ClusterClient](./kibana-plugin-server.clusterclient.md). Signature: ```typescript -export declare type IClusterClient = Pick; +export declare type IClusterClient = Pick; ``` diff --git a/docs/development/core/server/kibana-plugin-server.icustomclusterclient.md b/docs/development/core/server/kibana-plugin-server.icustomclusterclient.md new file mode 100644 index 0000000000000..d7511a119fc0f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.icustomclusterclient.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICustomClusterClient](./kibana-plugin-server.icustomclusterclient.md) + +## ICustomClusterClient type + +Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). + +See [ClusterClient](./kibana-plugin-server.clusterclient.md). + +Signature: + +```typescript +export declare type ICustomClusterClient = Pick; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md index 85874f5ec16ba..2e496aa0c46fc 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md @@ -10,7 +10,7 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | Buffer | Stream | { + custom: | { message: string | Error; attributes?: Record | undefined; } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; @@ -21,9 +21,9 @@ kibanaResponseFactory: { conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; - redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; - ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; - accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + redirected: (options: RedirectResponseOptions) => KibanaResponse>; + ok: (options?: HttpResponseOptions) => KibanaResponse>; + accepted: (options?: HttpResponseOptions) => KibanaResponse>; noContent: (options?: HttpResponseOptions) => KibanaResponse; } ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 5e7f84c55244d..5e28643843af3 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -17,7 +17,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Class | Description | | --- | --- | | [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path | -| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | +| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [CspConfig](./kibana-plugin-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | @@ -175,8 +175,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Headers](./kibana-plugin-server.headers.md) | Http request headers to read. | | [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) | Data send to the client as a response payload. | | [IBasePath](./kibana-plugin-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-server.basepath.md) | -| [IClusterClient](./kibana-plugin-server.iclusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | +| [IClusterClient](./kibana-plugin-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | +| [ICustomClusterClient](./kibana-plugin-server.icustomclusterclient.md) | Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | | [ISavedObjectsRepository](./kibana-plugin-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) | | [IScopedClusterClient](./kibana-plugin-server.iscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). | @@ -213,6 +214,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | +| [ScopeableRequest](./kibana-plugin-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-server.kibanarequest.md). | | [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) | | | [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | UI element type to represent the settings. | diff --git a/docs/development/core/server/kibana-plugin-server.scopeablerequest.md b/docs/development/core/server/kibana-plugin-server.scopeablerequest.md new file mode 100644 index 0000000000000..5a9443376996d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.scopeablerequest.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopeableRequest](./kibana-plugin-server.scopeablerequest.md) + +## ScopeableRequest type + +A user credentials container. It accommodates the necessary auth credentials to impersonate the current user. + +See [KibanaRequest](./kibana-plugin-server.kibanarequest.md). + +Signature: + +```typescript +export declare type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; +``` diff --git a/docs/limitations.asciidoc b/docs/limitations.asciidoc index 9bcba3b65d660..818cc766bf6a9 100644 --- a/docs/limitations.asciidoc +++ b/docs/limitations.asciidoc @@ -19,4 +19,4 @@ These {stack} features also have limitations that affect {kib}: include::limitations/nested-objects.asciidoc[] -include::limitations/export-data.asciidoc[] +include::limitations/export-data.asciidoc[] \ No newline at end of file diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 8d9ef515108ed..8e687f641c92b 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -1,17 +1,22 @@ [[index-patterns]] -== Index patterns +== Creating an index pattern -To visualize and explore data in {kib}, you must create an index pattern. -An index pattern tells {kib} which {es} indices contain the data that you want to work with. -An index pattern can match a single index, multiple indices, and a rollup index. +To explore and visualize data in {kib}, you must create an index pattern. +An index pattern tells {kib} which {es} indices contain the data that +you want to work with. +Once you create an index pattern, you're ready to: + +* Interactively explore your data in <>. +* Analyze your data in charts, tables, gauges, tag clouds, and more in <>. +* Show off your data in a <> workpad. +* If your data includes geo data, visualize it with <>. [float] [[index-patterns-read-only-access]] === [xpack]#Read-only access# -If you have insufficient privileges to create or save index patterns, a read-only +If you have insufficient privileges to create or save index patterns, a read-only indicator appears in Kibana. The buttons to create new index patterns or save -existing index patterns are not visible. For more information on granting access to -Kibana see <>. +existing index patterns are not visible. For more information, see <>. [role="screenshot"] image::images/management-index-read-only-badge.png[Example of Index Pattern Management's read only access indicator in Kibana's header] @@ -20,12 +25,9 @@ image::images/management-index-read-only-badge.png[Example of Index Pattern Mana [[settings-create-pattern]] === Create an index pattern -To get started, go to *Management > Kibana > Index Patterns*. You begin with -an overview of your index patterns, including any that were added when you -downloaded sample data sets. - -You can create a standard index pattern, and if a rollup index is detected in the -cluster, a rollup index pattern. +If you are in an app that requires an index pattern, and you don't have one yet, +{kib} prompts you to create one. Or, you can go directly to +*Management > Kibana > Index Patterns*. [role="screenshot"] image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollup index pattern"] @@ -33,83 +35,93 @@ image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollu [float] ==== Standard index pattern -{kib} makes it easy for you to create an index pattern by walking you through -the process. Just start typing in the *Index pattern* field, and {kib} looks for -the names of {es} indices that match your input. Make sure that the name of the +Just start typing in the *Index pattern* field, and {kib} looks for +the names of {es} indices that match your input. Make sure that the name of the index pattern is unique. - -If you want to include system indices in your search, toggle the switch in the -upper right. +To include system indices in your search, toggle the switch in the upper right. [role="screenshot"] image:management/index-patterns/images/create-index-pattern.png["Create index pattern"] -Your index pattern can match multiple {es} indices. -Use a comma to separate the names, with no space after the comma. The notation for -wildcards (`*`) and the ability to "exclude" (`-`) also apply +Your index pattern can match multiple {es} indices. +Use a comma to separate the names, with no space after the comma. The notation for +wildcards (`*`) and the ability to "exclude" (`-`) also apply (for example, `test*,-test3`). -When {kib} detects an index with a timestamp, you’re asked to choose a field to -filter your data by time. If you don’t specify a field, you won’t be able +If {kib} detects an index with a timestamp, you’re asked to choose a field to +filter your data by time. If you don’t specify a field, you won’t be able to use the time filter. -Once you’ve created your index pattern, you can start working with -your {es} data in {kib}. Here are some things to try: -* Interactively explore your data in <>. -* Present your data in charts, tables, gauges, tag clouds, and more in <>. -* Show off your data in a <> presentation. -* If your data includes geo data, visualize it using <>. - -For a walkthrough of creating an index pattern and visualizing the data, -see <>. [float] ==== Rollup index pattern -If a rollup index is detected in the cluster, clicking *Create index pattern* -includes an item for creating a rollup index pattern. You create an -index pattern for rolled up data the same way you do for any data. +If a rollup index is detected in the cluster, clicking *Create index pattern* +includes an item for creating a rollup index pattern. +You can match an index pattern to only rolled up data, or mix both rolled +up and raw data to explore and visualize all data together. +An index pattern can match +only one rollup index. + +[float] +[[management-cross-cluster-search]] +==== {ccs-cap} index pattern + +If your {es} clusters are configured for {ref}/modules-cross-cluster-search.html[{ccs}], you can create +index patterns to search across the clusters of your choosing. Using the +same syntax that you'd use in a raw {ccs} request in {es}, create your +index pattern with the convention `:`. + +For example, to query {ls} indices across two {es} clusters +that you set up for {ccs}, which are named `cluster_one` and `cluster_two`, +you would use `cluster_one:logstash-*,cluster_two:logstash-*` as your index pattern. + +You can use wildcards in your cluster names +to match any number of clusters, so if you want to search {ls} indices across +clusters named `cluster_foo`, `cluster_bar`, and so on, you would use `cluster_*:logstash-*` +as your index pattern. -You can match an index pattern to only rolled up data, or mix both rolled -up and raw data to visualize all data together. An index pattern can match -only one rollup index, not multiple. There is no restriction on the -number of standard indices that an index pattern can match. +To query across all {es} clusters that have been configured for {ccs}, +use a standalone wildcard for your cluster name in your index +pattern: `*:logstash-*`. -See <> -for more detailed information. +Once an index pattern is configured using the {ccs} syntax, all searches and +aggregations using that index pattern in {kib} take advantage of {ccs}. [float] === Manage your index pattern -Once you’ve created an index pattern, you’re presented a table of all fields -and associated data types in the index. +Once you create an index pattern, manually or with a sample data set, +you can look at its fields and associated data types. +You can also perform housekeeping tasks, such as making the +index pattern the default or deleting it when you longer need it. +To drill down into the details of an index pattern, click its name in +the *Index patterns* overview. [role="screenshot"] image:management/index-patterns/images/new-index-pattern.png["Index files and data types"] -You can perform the following actions: +From the detailed view, you can perform the following actions: -* *Manage the index fields.* Click a column header to sort the table by that column. -Use the field dropdown menu to limit to display to a specific field. -See <> for more detailed information. +* *Manage the index fields.* You can add formatters to format values and create +scripted fields. +See <> for more information. -* [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users -aware of which index pattern is the default. The first pattern -you create is automatically designated as the default pattern. The default -index pattern is loaded when you view the Discover tab. +* [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users +aware of which index pattern is the default. The first pattern +you create is automatically designated as the default pattern. The default +index pattern is loaded when you open *Discover*. -* [[reload-fields]]*Reload the index fields list.* You can reload the index fields list to -pick up any newly-added fields. Doing so also resets Kibana’s popularity counters -for the fields. The popularity counters keep track of the fields -you’ve used most often in {kib} and are used to sort fields in lists. +* [[reload-fields]]*Refresh the index fields list.* You can refresh the index fields list to +pick up any newly-added fields. Doing so also resets Kibana’s popularity counters +for the fields. The popularity counters are used in *Discover* to sort fields in lists. -* [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of -Saved Objects in {kib}. You will not be able to recover field formatters, +* [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of +Saved Objects in {kib}. You will not be able to recover field formatters, scripted fields, source filters, and field popularity data associated with the index pattern. -+ -Deleting an index pattern breaks all visualizations, saved searches, and -other saved objects that reference the pattern. Deleting an index pattern does +Deleting an index pattern does not remove any indices or data documents from {es}. - -include::index-patterns/management-cross-cluster-search.asciidoc[] ++ +WARNING: Deleting an index pattern breaks all visualizations, saved searches, and +other saved objects that reference the pattern. diff --git a/docs/management/index-patterns/management-cross-cluster-search.asciidoc b/docs/management/index-patterns/management-cross-cluster-search.asciidoc deleted file mode 100644 index 9fd8deb7f34be..0000000000000 --- a/docs/management/index-patterns/management-cross-cluster-search.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[[management-cross-cluster-search]] -=== {ccs-cap} - -{es} supports the ability to run search and aggregation requests across multiple -clusters using a module called _{ccs}_. - -In order to take advantage of {ccs}, you must configure your {es} -clusters accordingly. Review the corresponding {es} -{ref}/modules-cross-cluster-search.html[documentation] before attempting to use {ccs} in {kib}. - -Once your {es} clusters are configured for {ccs}, you can create -specific index patterns in {kib} to search across the clusters of your choosing. Using the -same syntax that you'd use in a raw {ccs} request in {es}, create your -index pattern in {kib} with the convention `:`. - -For example, if you want to query {ls} indices across two of the {es} clusters -that you set up for {ccs}, which were named `cluster_one` and `cluster_two`, -you would use `cluster_one:logstash-*,cluster_two:logstash-*` as your index pattern in {kib}. - -Just like in raw search requests in {es}, you can use wildcards in your cluster names -to match any number of clusters, so if you wanted to search {ls} indices across any -clusters named `cluster_foo`, `cluster_bar`, and so on, you would use `cluster_*:logstash-*` -as your index pattern in {kib}. - -If you want to query across all {es} clusters that have been configured for {ccs}, -then use a standalone wildcard for your cluster name in your {kib} index -pattern: `*:logstash-*`. - -Once an index pattern is configured using the {ccs} syntax, all searches and -aggregations using that index pattern in {kib} take advantage of {ccs}. diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index bbe4b3b68e03b..0ad27e68f7fe9 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -1,28 +1,185 @@ [[managing-licenses]] -== License Management +== License management -When you install {kib}, it generates a Basic license -with no expiration date. Go to *Management > License Management* to view the -status of your license, start a 30-day trial, or install a new license. +When you install the default distribution of {kib}, you receive a basic license +with no expiration date. For the full list of free features that are included in +the basic license, see https://www.elastic.co/subscriptions[the subscription page]. -To learn more about the available license levels, -see https://www.elastic.co/subscriptions[the subscription page]. +If you want to try out the full set of platinum features, you can activate a +30-day trial license. Go to *Management > License Management* to view the +status of your license, start a trial, or install a new license. -You can activate a 30-day trial license to try out the full set of -https://www.elastic.co/subscriptions[Platinum features], including machine learning, -advanced security, alerting, graph capabilities, and more. +NOTE: You can start a trial only if your cluster has not already activated a +trial license for the current major product version. For example, if you have +already activated a trial for v6.0, you cannot start a new trial until +v7.0. You can, however, contact `info@elastic.co` to request an extended trial +license. -When you activate a new license level, new features will appear in the left sidebar +When you activate a new license level, new features appear in the left sidebar of the *Management* page. [role="screenshot"] image::images/management-license.png[] -At the end of the trial period, the Platinum features operate in a -{stack-ov}/license-expiration.html[degraded mode]. You can revert to a Basic -license, extend the trial, or purchase a subscription. +At the end of the trial period, the platinum features operate in a +<>. You can revert to a basic license, +extend the trial, or purchase a subscription. +TIP: If {security-features} are enabled, before you revert to a basic license or +install a gold or platinum license, you must configure Transport Layer Security +(TLS) in {es}. See {ref}/encrypting-communications.html[Encrypting communications]. +{kib} and the {ref}/start-basic.html[start basic API] provide a list of all of +the features that will no longer be supported if you revert to a basic license. -TIP: If {security-features} are enabled, before you revert to a Basic license or install -a Gold or Platinum license, you must configure Transport Layer Security (TLS) in {es}. -See {ref}/encrypting-communications.html[Encrypting communications]. \ No newline at end of file +[discrete] +[[update-license]] +=== Update your license + +You can update your license at runtime without shutting down your {es} nodes. +License updates take effect immediately. The license is provided as a _JSON_ +file that you install in {kib} or by using the +{ref}/update-license.html[update license API]. + +TIP: If you are using a basic or trial license, {security-features} are disabled +by default. In all other licenses, {security-features} are enabled by default; +you must secure the {stack} or disable the {security-features}. + +[discrete] +[[license-expiration]] +=== License expiration + +Your license is time based and expires at a future date. If you're using +{monitor-features} and your license will expire within 30 days, a license +expiration warning is displayed prominently. Warnings are also displayed on +startup and written to the {es} log starting 30 days from the expiration date. +These error messages tell you when the license expires and what features will be +disabled if you do not update the license. + +IMPORTANT: You should update your license as soon as possible. You are +essentially flying blind when running with an expired license. Access to the +cluster health and stats APIs is critical for monitoring and managing an {es} +cluster. + +[discrete] +[[expiration-beats]] +==== Beats + +* Beats will continue to poll centrally-managed configuration. + +[discrete] +[[expiration-elasticsearch]] +==== {es} + +// Upgrade API is disabled +* The deprecation API is disabled. +* SQL support is disabled. +* Aggregations provided by the analytics plugin are no longer usable. + +[discrete] +[[expiration-watcher]] +==== {stack} {alert-features} + +* The PUT and GET watch APIs are disabled. The DELETE watch API continues to work. +* Watches execute and write to the history. +* The actions of the watches do not execute. + +[discrete] +[[expiration-graph]] +==== {stack} {graph-features} + +* Graph explore APIs are disabled. + +[discrete] +[[expiration-ml]] +==== {stack} {ml-features} + +* APIs to create {anomaly-jobs}, open jobs, send data to jobs, create {dfeeds}, +and start {dfeeds} are disabled. +* All started {dfeeds} are stopped. +* All open {anomaly-jobs} are closed. +* APIs to create and start {dfanalytics-jobs} are disabled. +* Existing {anomaly-job} and {dfanalytics-job} results continue to be available +by using {kib} or APIs. + +[discrete] +[[expiration-monitoring]] +==== {stack} {monitor-features} + +* The agent stops collecting cluster and indices metrics. +* The agent stops automatically cleaning indices older than +`xpack.monitoring.history.duration`. + +[discrete] +[[expiration-security]] +==== {stack} {security-features} + +* Cluster health, cluster stats, and indices stats operations are blocked. +* All data operations (read and write) continue to work. + +Once the license expires, calls to the cluster health, cluster stats, and index +stats APIs fail with a `security_exception` and return a 403 HTTP status code. + +[source,sh] +----------------------------------------------------- +{ + "error": { + "root_cause": [ + { + "type": "security_exception", + "reason": "current license is non-compliant for [security]", + "license.expired.feature": "security" + } + ], + "type": "security_exception", + "reason": "current license is non-compliant for [security]", + "license.expired.feature": "security" + }, + "status": 403 +} +----------------------------------------------------- + +This message enables automatic monitoring systems to easily detect the license +failure without immediately impacting other users. + +[discrete] +[[expiration-logstash]] +==== {ls} pipeline management + +* Cannot create new pipelines or edit or delete existing pipelines from the UI. +* Cannot list or view existing pipelines from the UI. +* Cannot run Logstash instances which are registered to listen to existing pipelines. +//TBD: * Logstash will continue to poll centrally-managed pipelines + +[discrete] +[[expiration-kibana]] +==== {kib} + +* Users can still log into {kib}. +* {kib} works for data exploration and visualization, but some features +are disabled. +* The license management UI is available to easily upgrade your license. See +<> and <>. + +[discrete] +[[expiration-reporting]] +==== {kib} {report-features} + +* Reporting is no longer available in {kib}. +* Report generation URLs stop working. +* Existing reports are no longer accessible. + +[discrete] +[[expiration-rollups]] +==== {rollups-cap} + +* {rollup-jobs-cap} cannot be created or started. +* Existing {rollup-jobs} can be stopped and deleted. +* The get rollup caps and rollup search APIs continue to function. + +[discrete] +[[expiration-transforms]] +==== {transforms-cap} + +* {transforms-cap} cannot be created, previewed, started, or updated. +* Existing {transforms} can be stopped and deleted. +* Existing {transform} results continue to be available. diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc index f19aaa122675e..dc722c24af76c 100644 --- a/docs/management/snapshot-restore/index.asciidoc +++ b/docs/management/snapshot-restore/index.asciidoc @@ -21,7 +21,7 @@ With this UI, you can: image:management/snapshot-restore/images/snapshot_list.png["Snapshot list"] Before using this feature, you should be familiar with how snapshots work. -{ref}/modules-snapshots.html[Snapshot and Restore] is a good source for +{ref}/snapshot-restore.html[Snapshot and Restore] is a good source for more detailed information. [float] @@ -35,9 +35,9 @@ registering one. {kib} supports three repository types out of the box: shared file system, read-only URL, and source-only. For more information on these repositories and their settings, -see {ref}/modules-snapshots.html#snapshots-repositories[Repositories]. +see {ref}/snapshots-register-repository.html[Repositories]. To use other repositories, such as S3, see -{ref}/modules-snapshots.html#_repository_plugins[Repository plugins]. +{ref}/snapshots-register-repository.html#snapshots-repository-plugins[Repository plugins]. Once you create a repository, it is listed in the *Repositories* @@ -61,7 +61,7 @@ into each snapshot for further investigation. image:management/snapshot-restore/images/snapshot_details.png["Snapshot details"] If you don’t have any snapshots, you can create them from the {kib} <>. The -{ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API] +{ref}/snapshots-take-snapshot.html[snapshot API] takes the current state and data in your index or cluster, and then saves it to a shared repository. @@ -162,7 +162,7 @@ Ready to try *Snapshot and Restore*? In this tutorial, you'll learn to: This example shows you how to register a shared file system repository and store snapshots. Before you begin, you must register the location of the repository in the -{ref}/modules-snapshots.html#_shared_file_system_repository[path.repo] setting on +{ref}/snapshots-register-repository.html#snapshots-filesystem-repository[path.repo] setting on your master and data nodes. You can do this in one of two ways: * Edit your `elasticsearch.yml` to include the `path.repo` setting. @@ -197,7 +197,7 @@ The repository currently doesn’t have any snapshots. [float] ==== Add a snapshot to the repository -Use the {ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API] to create a snapshot. +Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot. . Go to *Dev Tools > Console*. . Create the snapshot: @@ -206,7 +206,7 @@ Use the {ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API] to c PUT /_snapshot/my_backup/2019-04-25_snapshot?wait_for_completion=true + In this example, the snapshot name is `2019-04-25_snapshot`. You can also -use {ref}//date-math-index-names.html[date math expression] for the snapshot name. +use {ref}/date-math-index-names.html[date math expression] for the snapshot name. + [role="screenshot"] image:management/snapshot-restore/images/create_snapshot.png["Create snapshot"] diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 8d28b55a6502f..a6eeffec51cb0 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -5,9 +5,23 @@ APM settings ++++ -You do not need to configure any settings to use APM. It is enabled by default. -If you'd like to change any of the default values, -copy and paste the relevant settings below into your `kibana.yml` configuration file. +You do not need to configure any settings to use the APM app. It is enabled by default. + +[float] +[[apm-indices-settings-kb]] +==== APM Indices + +// This content is reused in the APM app documentation. +// Any changes made in this file will be seen there as well. +// tag::apm-indices-settings[] + +Index defaults can be changed in Kibana. Navigate to *APM* > *Settings* > *Indices*. +Index settings in the APM app take precedence over those set in `kibana.yml`. + +[role="screenshot"] +image::settings/images/apm-settings.png[APM app settings in Kibana] + +// end::apm-indices-settings[] [float] [[general-apm-settings-kb]] @@ -17,6 +31,9 @@ copy and paste the relevant settings below into your `kibana.yml` configuration // Any changes made in this file will be seen there as well. // tag::general-apm-settings[] +If you'd like to change any of the default values, +copy and paste the relevant settings below into your `kibana.yml` configuration file. + xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to `true`. diff --git a/docs/settings/images/apm-settings.png b/docs/settings/images/apm-settings.png new file mode 100644 index 0000000000000..876f135da9356 Binary files /dev/null and b/docs/settings/images/apm-settings.png differ diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 2fc74d2ffee32..38a46a3cde5a0 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -66,13 +66,6 @@ both the {es} monitoring cluster and the {es} production cluster. If not set, {kib} uses the value of the `elasticsearch.password` setting. -`telemetry.enabled`:: -Set to `true` (default) to send cluster statistics to Elastic. Reporting your -cluster statistics helps us improve your user experience. Your data is never -shared with anyone. Set to `false` to disable statistics reporting from any -browser connected to the {kib} instance. You can also opt out through the -*Advanced Settings* in {kib}. - `xpack.monitoring.elasticsearch.pingTimeout`:: Specifies the time in milliseconds to wait for {es} to respond to internal health checks. By default, it matches the `elasticsearch.pingTimeout` setting, diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index a754f91e9f22a..a9fa2bd18d315 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -54,6 +54,14 @@ The protocol for accessing Kibana, typically `http` or `https`. `xpack.reporting.kibanaServer.hostname`:: The hostname for accessing {kib}, if different from the `server.host` value. +[NOTE] +============ +Reporting authenticates requests on the Kibana page only when the hostname matches the +`xpack.reporting.kibanaServer.hostname` setting. Therefore Reporting would fail if the +set value redirects to another server. For that reason, `"0"` is an invalid setting +because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`. +============ + [float] [[reporting-job-queue-settings]] diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index d6dd4378da1b7..16d68a7759f77 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -45,7 +45,7 @@ if this setting isn't the same for all instances of {kib}. `xpack.security.secureCookies`:: Sets the `secure` flag of the session cookie. The default value is `false`. It -is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set +is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 01e6bd51ea50b..535ad16978217 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -82,31 +82,52 @@ Elasticsearch nodes on startup. `elasticsearch.sniffOnConnectionFault:`:: *Default: false* Update the list of Elasticsearch nodes immediately following a connection fault. -`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls -whether to always present the certificate specified by -`elasticsearch.ssl.certificate` when requested. This applies to all requests to -Elasticsearch, including requests that are proxied for end-users. Setting this -to `true` when Elasticsearch is using certificates to authenticate users can -lead to proxied requests for end-users being executed as the identity tied to -the configured certificate. - -`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Optional -settings that provide the paths to the PEM-format SSL certificate and key files. -These files are used to verify the identity of Kibana to Elasticsearch and are -required when `xpack.security.http.ssl.client_authentication` in Elasticsearch is -set to `required`. - -`elasticsearch.ssl.certificateAuthorities:`:: Optional setting that enables you -to specify a list of paths to the PEM file for the certificate authority for -your Elasticsearch instance. - -`elasticsearch.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt -the private key. This value is optional as the key may not be encrypted. - -`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the -verification of certificates presented by Elasticsearch. Valid values are `none`, -`certificate`, and `full`. `full` performs hostname verification, and -`certificate` does not. +`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls whether to always present the certificate specified by +`elasticsearch.ssl.certificate` or `elasticsearch.ssl.keystore.path` when requested. This setting applies to all requests to Elasticsearch, +including requests that are proxied for end users. Setting this to `true` when Elasticsearch is using certificates to authenticate users can +lead to proxied requests for end users being executed as the identity tied to the configured certificate. + +`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. +When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required` or `optional`, the certificate and key are used +to prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. ++ +-- +NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. +-- + +`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. These certificates may consist of a root +certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is used to +establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may +be specified via `elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. + +`elasticsearch.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via +`elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted. + +`elasticsearch.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. When +`xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required` or `optional`, the certificate and key are used to +prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. If the file contains any additional certificates, +those will be used as a trusted certificate chain for your Elasticsearch cluster. This chain is used to establish trust when Kibana creates +an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. ++ +-- +NOTE: This setting cannot be used in conjunction with `elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. +-- + +`elasticsearch.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has +no password, leave this unset. If your key store has an empty password, set this to `""`. + +`elasticsearch.ssl.truststore.path:`:: Path to a PKCS #12 trust store that contains one or more X.509 certificates. This may consist of a +root certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for your Elasticsearch cluster. +This chain is used to establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, +trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`. + +`elasticsearch.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, +leave this unset. If your trust store has an empty password, set this to `""`. + +`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the verification of certificates presented by Elasticsearch. Valid values +are `none`, `certificate`, and `full`. `full` performs hostname verification and `certificate` does not. This setting is used only when +traffic to Elasticsearch is encrypted, which is specified by using the HTTPS protocol in `elasticsearch.hosts`. `elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying. @@ -325,11 +346,19 @@ default is `true`. `server.socketTimeout:`:: *Default: "120000"* The number of milliseconds to wait before closing an inactive socket. -`server.ssl.certificate:` and `server.ssl.key:`:: Paths to the PEM-format SSL -certificate and SSL key files, respectively. +`server.ssl.certificate:` and `server.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. These are used +when enabling SSL for inbound requests from web browsers to the Kibana server. ++ +-- +NOTE: These settings cannot be used in conjunction with `server.ssl.keystore.path`. +-- -`server.ssl.certificateAuthorities:`:: List of paths to PEM encoded certificate -files that should be trusted. +`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. These certificates may consist of a root +certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is used when a +web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity +certificate to establish trust. This chain is also used to determine whether client certificates should be trusted when PKI authentication +is enabled. In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or +`server.ssl.truststore.path`. `server.ssl.cipherSuites:`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*. Details on the format, and the valid options, are available via the @@ -339,12 +368,36 @@ https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenS connections. Valid values are `required`, `optional`, and `none`. `required` forces a client to present a certificate, while `optional` requests a client certificate but the client is not required to present one. -`server.ssl.enabled:`:: *Default: "false"* Enables SSL for outgoing requests -from the Kibana server to the browser. When set to `true`, -`server.ssl.certificate` and `server.ssl.key` are required. +`server.ssl.enabled:`:: *Default: "false"* Enables SSL for inbound requests from the browser to the Kibana server. When set to `true`, a +certificate and private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of +`server.ssl.certificate` and `server.ssl.key`. + +`server.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via `server.ssl.key`. This value +is optional, as the key may not be encrypted. + +`server.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. These are used when enabling +SSL for inbound requests from web browsers to the Kibana server. If the file contains any additional certificates, those will be used as a +trusted certificate chain for Kibana. This chain is used when a web browser creates an SSL connection with the Kibana server; the +certificate chain is sent to the browser along with the end-entity certificate to establish trust. This chain is also used to determine +whether client certificates should be trusted when PKI authentication is enabled. In addition to this setting, trusted certificates may be +specified via `server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`. ++ +-- +NOTE: This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key`. +-- -`server.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the -private key. This value is optional as the key may not be encrypted. +`server.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has no +password, leave this unset. If your key store has an empty password, set this to `""`. + +`server.ssl.truststore.path:`:: Path to a PKCS #12 trust store that contains one or more X.509 certificates. These certificates may consist +of a root certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is +used when a web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the +end-entity certificate to establish trust. This chain is also used to determine whether client certificates should be trusted when PKI +authentication is enabled. In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or +`server.ssl.keystore.path`. + +`server.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, leave +this unset. If your trust store has an empty password, set this to `""`. `server.ssl.redirectHttpFromPort:`:: Kibana will bind to this port and redirect all http requests to https over the port configured as `server.port`. @@ -380,6 +433,11 @@ cannot be `false` at the same time. To enable telemetry and prevent users from disabling it, set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +`telemetry.enabled`:: *Default: true* Reporting your cluster statistics helps +us improve your user experience. Your data is never shared with anyone. Set to +`false` to disable telemetry capabilities entirely. You can alternatively opt +out through the *Advanced Settings* in {kib}. + `vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to diff --git a/docs/spaces/images/spaces-configure-landing-page.png b/docs/spaces/images/spaces-configure-landing-page.png new file mode 100644 index 0000000000000..15006594b6d7b Binary files /dev/null and b/docs/spaces/images/spaces-configure-landing-page.png differ diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 69655aac521e7..fb5ef670692dc 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -2,13 +2,13 @@ [[xpack-spaces]] == Spaces -Spaces enable you to organize your dashboards and other saved -objects into meaningful categories. Once inside a space, you see only -the dashboards and saved objects that belong to that space. +Spaces enable you to organize your dashboards and other saved +objects into meaningful categories. Once inside a space, you see only +the dashboards and saved objects that belong to that space. -{kib} creates a default space for you. -After you create your own -spaces, you're asked to choose a space when you log in to Kibana. You can change your +{kib} creates a default space for you. +After you create your own +spaces, you're asked to choose a space when you log in to Kibana. You can change your current space at any time by using the menu in the upper left. [role="screenshot"] @@ -29,24 +29,24 @@ Kibana supports spaces in several ways. You can: [[spaces-managing]] === View, create, and delete spaces -Go to **Management > Spaces** for an overview of your spaces. This view provides actions +Go to **Management > Spaces** for an overview of your spaces. This view provides actions for you to create, edit, and delete spaces. [role="screenshot"] image::spaces/images/space-management.png["Space management"] [float] -==== Create or edit a space +==== Create or edit a space -You can create as many spaces as you like. Click *Create a space* and provide a name, -URL identifier, optional description. +You can create as many spaces as you like. Click *Create a space* and provide a name, +URL identifier, optional description. -The URL identifier is a short text string that becomes part of the -{kib} URL when you are inside that space. {kib} suggests a URL identifier based +The URL identifier is a short text string that becomes part of the +{kib} URL when you are inside that space. {kib} suggests a URL identifier based on the name of your space, but you can customize the identifier to your liking. You cannot change the space identifier once you create the space. -{kib} also has an <> +{kib} also has an <> if you prefer to create spaces programatically. [role="screenshot"] @@ -55,7 +55,7 @@ image::spaces/images/edit-space.png["Space management"] [float] ==== Delete a space -Deleting a space permanently removes the space and all of its contents. +Deleting a space permanently removes the space and all of its contents. Find the space on the *Spaces* overview page and click the trash icon in the Actions column. You can't delete the default space, but you can customize it to your liking. @@ -63,14 +63,14 @@ You can't delete the default space, but you can customize it to your liking. [[spaces-control-feature-visibility]] === Control feature access based on user needs -You have control over which features are visible in each space. -For example, you might hide Dev Tools +You have control over which features are visible in each space. +For example, you might hide Dev Tools in your "Executive" space or show Stack Monitoring only in your "Admin" space. You can define which features to show or hide when you add or edit a space. -Controlling feature -visibility is not a security feature. To secure access -to specific features on a per-user basis, you must configure +Controlling feature +visibility is not a security feature. To secure access +to specific features on a per-user basis, you must configure <>. [role="screenshot"] @@ -80,10 +80,10 @@ image::spaces/images/edit-space-feature-visibility.png["Controlling features vis [[spaces-control-user-access]] === Control feature access based on user privileges -When using Kibana with security, you can configure applications and features -based on your users’ privileges. This means different roles can have access -to different features in the same space. -Power users might have privileges to create and edit visualizations and dashboards, +When using Kibana with security, you can configure applications and features +based on your users’ privileges. This means different roles can have access +to different features in the same space. +Power users might have privileges to create and edit visualizations and dashboards, while analysts or executives might have Dashboard and Canvas with read-only privileges. See <> for details. @@ -106,7 +106,7 @@ interface. . Import your saved objects. . (Optional) Delete objects in the export space that you no longer need. -{kib} also has beta <> and +{kib} also has beta <> and <> APIs if you want to automate this process. [float] @@ -115,17 +115,22 @@ interface. You can create a custom experience for users by configuring the {kib} landing page on a per-space basis. The landing page can route users to a specific dashboard, application, or saved object as they enter each space. -To configure the landing page, use the `defaultRoute` setting in < Advanced settings>>. + +To configure the landing page, use the default route setting in < Advanced settings>>. +For example, you might set the default route to `/app/kibana#/dashboards`. + +[role="screenshot"] +image::spaces/images/spaces-configure-landing-page.png["Configure space-level landing page"] + [float] [[spaces-delete-started]] === Disable and version updates -Spaces are automatically enabled in {kib}. If you don't want use this feature, +Spaces are automatically enabled in {kib}. If you don't want use this feature, you can disable it -by setting `xpack.spaces.enabled` to `false` in your +by setting `xpack.spaces.enabled` to `false` in your `kibana.yml` configuration file. -If you are upgrading your -version of {kib}, the default space will contain all of your existing saved objects. - +If you are upgrading your +version of {kib}, the default space will contain all of your existing saved objects. diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 36d6b0a6e473a..7de7d73bf1664 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -3,6 +3,7 @@ [partintro] -- + When you know what your data includes, you can create visualizations that best display that data and build better dashboards. *Discover* enables you to explore your data, find @@ -99,6 +100,8 @@ or create a direct link to share. The *Save* and *Share* actions are in the men -- +include::{kib-repo-dir}/management/index-patterns.asciidoc[] + include::{kib-repo-dir}/discover/set-time-filter.asciidoc[] include::{kib-repo-dir}/discover/search.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index d1acb915f1973..2c41d0072fe5b 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -13,8 +13,6 @@ visualizations, and dashboards. include::{kib-repo-dir}/management/managing-licenses.asciidoc[] -include::{kib-repo-dir}/management/index-patterns.asciidoc[] - include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] @@ -40,5 +38,3 @@ include::{kib-repo-dir}/management/managing-beats.asciidoc[] include::{kib-repo-dir}/management/managing-remote-clusters.asciidoc[] include::{kib-repo-dir}/management/snapshot-restore/index.asciidoc[] - - diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index d7af0d5c420a1..b5d263aed8346 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -96,7 +96,8 @@ used when {kib} sends monitoring data to the production cluster. .. Configure {kib} to encrypt communications between the {kib} server and the production cluster. This set up involves generating a server certificate and setting `server.ssl.*` and `elasticsearch.ssl.certificateAuthorities` settings -in the `kibana.yml` file on the {kib} server. For example: +in the `kibana.yml` file on the {kib} server. For example, using a PEM-formatted +certificate and private key: + -- [source,yaml] @@ -105,14 +106,19 @@ server.ssl.key: /path/to/your/server.key server.ssl.certificate: /path/to/your/server.crt -------------------------------------------------------------------------------- -If you are using your own certificate authority to sign certificates, specify -the location of the PEM file in the `kibana.yml` file: +If you are using your own certificate authority (CA) to sign certificates, +specify the location of the PEM file in the `kibana.yml` file: [source,yaml] -------------------------------------------------------------------------------- elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem -------------------------------------------------------------------------------- +NOTE: Alternatively, the PKCS #12 format can be used for the Kibana certificate +and key, along with any included CA certificates, by setting +`server.ssl.keystore.path`. If your CA certificate chain is in a separate trust +store, you can also use `server.ssl.truststore.path`. + For more information, see <>. -- diff --git a/docs/user/reporting/images/shareable-container.png b/docs/user/reporting/images/shareable-container.png new file mode 100644 index 0000000000000..db5a41dcff471 Binary files /dev/null and b/docs/user/reporting/images/shareable-container.png differ diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 06af9e6038445..fde88130a26b4 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -6,11 +6,11 @@ -- -You can generate a report that contains a {kib} dashboard, visualization, -saved search, or Canvas workpad. Depending on the object type, you can export the data as +You can generate a report that contains a {kib} dashboard, visualization, +saved search, or Canvas workpad. Depending on the object type, you can export the data as a PDF, PNG, or CSV document, which you can keep for yourself, or share with others. -Reporting is available from the *Share* menu +Reporting is available from the *Share* menu in *Discover*, *Visualize*, *Dashboard*, and *Canvas*. [role="screenshot"] @@ -40,9 +40,9 @@ for an example. [[manually-generate-reports]] == Generate a report manually -. Open the dashboard, visualization, Canvas workpad, or saved search that you want to include in the report. +. Open the dashboard, visualization, Canvas workpad, or saved search that you want to include in the report. -. In the {kib} toolbar, click *Share*. If you are working in Canvas, +. In the {kib} toolbar, click *Share*. If you are working in Canvas, click the share icon image:user/reporting/images/canvas-share-button.png["Canvas Share button"]. . Select the option appropriate for your object. You can export: @@ -55,14 +55,36 @@ click the share icon image:user/reporting/images/canvas-share-button.png["Canvas + A notification appears when the report is complete. +[float] +[[reporting-layout-sizing]] +== Layout and sizing +The layout and size of the PDF or PNG image depends on the {kib} app +with which the Reporting plugin is integrated. For Canvas, the +worksheet dimensions determine the size for Reporting. In other apps, +the dimensions are taken on the fly by looking at +the size of the visualization elements or panels on the page. + +The size dimensions are part of the reporting job parameters. Therefore, to +make the report output larger or smaller, you can change the size of the browser. +This resizes the shareable container before generating the +report, so the desired dimensions are passed in the job parameters. + +In the following {kib} dashboard, the shareable container is highlighted. +The shareable container is captured when you click the +*Generate* or *Copy POST URL* button. It might take some trial and error +before you're satisfied with the layout and dimensions in the resulting +PNG or PDF image. + +[role="screenshot"] +image::user/reporting/images/shareable-container.png["Shareable Container"] + + + [float] [[optimize-pdf]] == Optimize PDF for print—dashboard only -By default, {kib} creates a PDF -using the existing layout and size of the dashboard. To create a -printer-friendly PDF with multiple A4 portrait pages and two visualizations -per page, turn on *Optimize for printing*. +To create a printer-friendly PDF with multiple A4 portrait pages and two visualizations per page, turn on *Optimize for printing*. [role="screenshot"] image::user/reporting/images/preserve-layout-switch.png["Share"] @@ -72,8 +94,8 @@ image::user/reporting/images/preserve-layout-switch.png["Share"] [[manage-report-history]] == View and manage report history -For a list of your reports, go to *Management > Reporting*. -From this view, you can monitor the generation of a report and +For a list of your reports, go to *Management > Reporting*. +From this view, you can monitor the generation of a report and download reports that you previously generated. [float] diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index ca7fa6abcc9d9..dc4ffdfebdae9 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -7,12 +7,20 @@ Having trouble? Here are solutions to common problems you might encounter while using Reporting. +* <> +* <> +* <> +* <> +* <> +* <> +* <> + [float] [[reporting-troubleshooting-system-dependencies]] === System dependencies Reporting launches a "headless" web browser called Chromium on the Kibana server. It is a custom build made by Elastic of an open source project, and it is intended to have minimal dependencies on OS libraries. However, the Kibana server OS might still require additional -dependencies for Chromium. +dependencies to run the Chromium executable. Make sure Kibana server OS has the appropriate packages installed for the distribution. @@ -33,19 +41,30 @@ If you are using Ubuntu/Debian systems, install the following packages: * `fonts-liberation` * `libfontconfig1` +If the system is missing dependencies, then Reporting will fail in a non-deterministic way. {kib} runs a self-test at server startup, and +if it encounters errors, logs them in the Console. Unfortunately, the error message does not include +information about why Chromium failed to run. The most common error message is `Error: connect ECONNREFUSED`, which indicates +that {kib} could not connect to the Chromium process. + +To troubleshoot the problem, start the {kib} server with environment variables that tell Chromium to print verbose logs. See the +<> for more information. + [float] -=== Text is rendered incorrectly in generated reports +[[reporting-troubleshooting-text-incorrect]] +=== Text rendered incorrectly in generated reports If a report label is rendered as an empty rectangle, no system fonts are available. Install at least one font package on the system. If the report is missing certain Chinese, Japanese or Korean characters, ensure that a system font with those characters is installed. [float] +[[reporting-troubleshooting-missing-data]] === Missing data in PDF report of data table visualization There is currently a known limitation with the Data Table visualization that only the first page of data rows, which are the only data visible on the screen, are shown in PDF reports. [float] +[[reporting-troubleshooting-file-permissions]] === File permissions Ensure that the `headless_shell` binary located in your Kibana data directory is owned by the user who is running Kibana, that the user has the execute permission, and if applicable, that the filesystem is mounted with the `exec` option. @@ -63,25 +82,25 @@ Whenever possible, a Reporting error message tries to be as self-explanatory as along with the solution. [float] -==== "Max attempts reached" +==== Max attempts reached There are two primary causes of this error: -. You're creating a PDF of a visualization or dashboard that spans a large amount of data and Kibana is hitting the `xpack.reporting.queue.timeout` +* You're creating a PDF of a visualization or dashboard that spans a large amount of data and Kibana is hitting the `xpack.reporting.queue.timeout` -. Kibana is hosted behind a reverse-proxy, and the <> are not configured correctly +* Kibana is hosted behind a reverse-proxy, and the <> are not configured correctly Create a Markdown visualization and then create a PDF report. If this succeeds, increase the `xpack.reporting.queue.timeout` setting. If the PDF report fails with "Max attempts reached," check your <>. [float] [[reporting-troubleshooting-nss-dependency]] -==== "You must install nss for Reporting to work" +==== You must install nss for Reporting to work Reporting using the Chromium browser relies on the Network Security Service libraries (NSS). Install the appropriate nss package for your distribution. [float] [[reporting-troubleshooting-sandbox-dependency]] -==== "Unable to use Chromium sandbox" +==== Unable to use Chromium sandbox Chromium uses sandboxing techniques that are built on top of operating system primitives. The Linux sandbox depends on user namespaces, which were introduced with the 3.8 Linux kernel. However, many distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. @@ -90,6 +109,7 @@ Elastic recommends that you research the feasibility of enabling unprivileged us is if you are running Kibana in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters. [float] +[[reporting-troubleshooting-verbose-logs]] === Verbose logs {kib} server logs have a lot of useful information for troubleshooting and understanding how things work. If you're having any issues at all, the full logs from Reporting will be the first place to look. In `kibana.yml`: @@ -101,10 +121,12 @@ logging.verbose: true For more information about logging, see <>. +[float] +[[reporting-troubleshooting-puppeteer-debug-logs]] === Puppeteer debug logs The Chromium browser that {kib} launches on the server is driven by a NodeJS library for Chromium called Puppeteer. The Puppeteer library has its own command-line method to generate its own debug logs, which can sometimes be helpful, particularly to figure out if a problem is -caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips +caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips[debugging tips]. Using Puppeteer's debug method when launching Kibana would look like: ``` @@ -114,3 +136,14 @@ The internal DevTools protocol traffic will be logged via the `debug` module und The Puppeteer logs are very verbose and could possibly contain sensitive information. Handle the generated output with care. + +[float] +[[reporting-troubleshooting-system-requirements]] +=== System requirements +In Elastic Cloud, the {kib} instances that most configurations provide by default is for 1GB of RAM for the instance. That is enough for +{kib} Reporting when the visualization or dashboard is relatively simple, such as a single pie chart or a dashboard with +a few visualizations. However, certain visualization types incur more load than others. For example, a TSVB panel has a lot of network +requests to render. + +If the {kib} instance doesn't have enough memory to run the report, the report fails with an error such as `Error: Page crashed!` +In this case, try increasing the memory for the {kib} instance to 2GB. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 2e2aaf688e8b6..05aabfc343be9 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -67,6 +67,9 @@ server.ssl.clientAuthentication: required xpack.security.authc.providers: [pki] -------------------------------------------------------------------------------- +NOTE: Trusted CAs can also be specified in a PKCS #12 keystore bundled with your Kibana server certificate/key using +`server.ssl.keystore.path` or in a separate trust store using `server.ssl.truststore.path`. + PKI support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both PKI and Basic authentication for the same {kib} instance: [source,yaml] diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index eab3833b3f5ae..e3d6e0d97c73a 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -37,4 +37,5 @@ cause Kibana's authorization to behave unexpectedly. include::authorization/index.asciidoc[] include::authorization/kibana-privileges.asciidoc[] include::api-keys/index.asciidoc[] +include::role-mappings/index.asciidoc[] include::rbac_tutorial.asciidoc[] diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 86599be9af375..c2ed295e83ce9 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -57,6 +57,30 @@ that provides read and write privileges in Go to *Management > Users*, add a new user, and assign the user the built-in `reporting_user` role and your new custom role, `custom_reporting_user`. +[float] +==== With a custom index + +If you are using Reporting with a custom index, +the `xpack.reporting.index` setting should begin +with `.reporting-*`. The default {kib} system user has +`all` privileges against the `.reporting-*` pattern of indices. + +[source,js] +xpack.reporting.index: '.reporting-custom-index' + +If you use a different pattern for the `xpack.reporting.index` setting, +you must create a custom role with appropriate access to the index, similar +to the following: + +. Go to *Management > Roles*, and click *Create role*. +. Name the role `custom-reporting-user`. +. Specify the custom index and assign it the `all` index privilege. +. Go to *Management > Users* and create a new user with +the `kibana_system` role and the `custom-reporting-user` role. +. Configure {kib} to use the new account: +[source,js] +elasticsearch.username: 'custom_kibana_system' + [float] [[reporting-roles-user-api]] ==== With the user API diff --git a/docs/user/security/role-mappings/images/role-mappings-create-step-1.png b/docs/user/security/role-mappings/images/role-mappings-create-step-1.png new file mode 100644 index 0000000000000..2b4ad16459529 Binary files /dev/null and b/docs/user/security/role-mappings/images/role-mappings-create-step-1.png differ diff --git a/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif b/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif new file mode 100644 index 0000000000000..0a10126ea3cce Binary files /dev/null and b/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif differ diff --git a/docs/user/security/role-mappings/images/role-mappings-grid.png b/docs/user/security/role-mappings/images/role-mappings-grid.png new file mode 100644 index 0000000000000..96c9ee8e4cd95 Binary files /dev/null and b/docs/user/security/role-mappings/images/role-mappings-grid.png differ diff --git a/docs/user/security/role-mappings/index.asciidoc b/docs/user/security/role-mappings/index.asciidoc new file mode 100644 index 0000000000000..01028ab4d59e0 --- /dev/null +++ b/docs/user/security/role-mappings/index.asciidoc @@ -0,0 +1,51 @@ +[role="xpack"] +[[role-mappings]] +=== Role mappings + +Role mappings allow you to describe which roles to assign to your users +using a set of rules. Role mappings are required when authenticating via +an external identity provider, such as Active Directory, Kerberos, PKI, OIDC, +or SAML. + +Role mappings have no effect for users inside the `native` or `file` realms. + +To manage your role mappings, use *Management > Security > Role Mappings*. + +With *Role mappings*, you can: + +* View your configured role mappings +* Create/Edit/Delete role mappings + +[role="screenshot"] +image:user/security/role-mappings/images/role-mappings-grid.png["Role mappings"] + + +[float] +=== Create a role mapping + +To create a role mapping, navigate to *Management > Security > Role Mappings*, and click **Create role mapping**. +Give your role mapping a unique name, and choose which roles you wish to assign to your users. +If you need more flexibility, you can use {ref}/security-api-put-role-mapping.html#_role_templates[role templates] instead. + +Next, define the rules describing which users should receive the roles you defined. Rules can optionally grouped and nested, allowing for sophisticated logic to suite complex requirements. +View the {ref}/role-mapping-resources.html[role mapping resources for an overview of the allowed rule types]. + + +[float] +=== Example + +Let's create a `sales-users` role mapping, which assigns a `sales` role to users whose username +starts with `sls_`, *or* belongs to the `executive` group. + +First, we give the role mapping a name, and assign the `sales` role: + +[role="screenshot"] +image:user/security/role-mappings/images/role-mappings-create-step-1.png["Create role mapping, step 1"] + +Next, we define the two rules, making sure to set the group to *Any are true*: + +[role="screenshot"] +image:user/security/role-mappings/images/role-mappings-create-step-2.gif["Create role mapping, step 2"] + +Click *Save role mapping* once you're finished. + diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index 6917a48909c7b..b370c35905bce 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -4,121 +4,115 @@ Encrypting communications ++++ -{kib} supports Transport Layer Security (TLS/SSL) encryption for client -requests. -//TBD: It is unclear what "client requests" are in this context. Is it just -// communication between the browser and the Kibana server or are we talking -// about other types of clients connecting to the Kibana server? - -If you are using {security} or a proxy that provides an HTTPS endpoint for {es}, -you can configure {kib} to access {es} via HTTPS. Thus, communications between -{kib} and {es} are also encrypted. - -. Configure {kib} to encrypt communications between the browser and the {kib} -server: +{kib} supports Transport Layer Security (TLS/SSL) encryption for all forms of data-in-transit. Browsers send traffic to {kib} and {kib} +sends traffic to {es}. These communications are configured separately. + +[[configuring-tls-browser-kib]] +==== Encrypting traffic between the browser and {kib} + +NOTE: You do not need to enable {security-features} for this type of encryption. + +. Obtain a server certificate and private key for {kib}. + -- -NOTE: You do not need to enable {security} for this type of encryption. +{kib} supports certificates/keys in both PKCS #12 key stores and PEM format. + +When you obtain a certificate, you must do at least one of the following: + +.. Set the certificate's `subjectAltName` to the hostname, fully-qualified domain name (FQDN), or IP address of the {kib} server. + +.. Set the certificate's Common Name (CN) to the {kib} server's hostname or FQDN. Using the server's IP address as the CN does not work. + +You may choose to generate a certificate and private key using {ref}/certutil.html[the {es} certutil tool]. If you already used certutil to +generate a certificate authority (CA), you would generate a certificate/key for Kibana like so (using the `--dns` param to set the +`subjectAltName`): + +[source,sh] +-------------------------------------------------------------------------------- +bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12 --name kibana --dns localhost +-------------------------------------------------------------------------------- + +This will generate a certificate and private key in a PKCS #12 keystore named `kibana.p12`. -- -.. Generate a server certificate for {kib}. +. Enable TLS/SSL in `kibana.yml`: + -- -//TBD: Can we provide more information about how they generate the certificate? -//Would they be able to use something like the elasticsearch-certutil command? -You must either set the certificate's -`subjectAltName` to the hostname, fully-qualified domain name (FQDN), or IP -address of the {kib} server, or set the CN to the {kib} server's hostname -or FQDN. Using the server's IP address as the CN does not work. +[source,yaml] +-------------------------------------------------------------------------------- +server.ssl.enabled: true +-------------------------------------------------------------------------------- -- -.. Set the `server.ssl.enabled`, `server.ssl.key`, and `server.ssl.certificate` -properties in `kibana.yml`: +. Specify your server certificate and private key in `kibana.yml`: + -- +If your certificate and private key are in a PKCS #12 keystore, specify it like so: + [source,yaml] -------------------------------------------------------------------------------- -server.ssl.enabled: true -server.ssl.key: /path/to/your/server.key -server.ssl.certificate: /path/to/your/server.crt +server.ssl.keystore.path: "/path/to/your/keystore.p12" +server.ssl.keystore.password: "optional decryption password" +-------------------------------------------------------------------------------- + +Otherwise, if your certificate/key are in PEM format, specify them like so: + +[source,yaml] +-------------------------------------------------------------------------------- +server.ssl.certificate: "/path/to/your/server.crt" +server.ssl.key: "/path/to/your/server.key" +server.ssl.keyPassphrase: "optional decryption password" -------------------------------------------------------------------------------- After making these changes, you must always access {kib} via HTTPS. For example, https://localhost:5601. -// TBD: The reference information for server.ssl.enabled says it "enables SSL for -// outgoing requests from the Kibana server to the browser". Do we need to -// reiterate here that only one side of the communications is encrypted? - For more information, see <>. -- -. Configure {kib} to connect to {es} via HTTPS: -+ --- +[[configuring-tls-kib-es]] +==== Encrypting traffic between {kib} and {es} + NOTE: To perform this step, you must {ref}/configuring-security.html[enable the {es} {security-features}] or you must have a proxy that provides an HTTPS endpoint for {es}. --- - -.. Specify the HTTPS protocol in the `elasticsearch.hosts` setting in the {kib} -configuration file, `kibana.yml`: +. Specify the HTTPS URL in the `elasticsearch.hosts` setting in the {kib} configuration file, `kibana.yml`: + -- [source,yaml] -------------------------------------------------------------------------------- elasticsearch.hosts: ["https://.com:9200"] -------------------------------------------------------------------------------- --- - -.. If you are using your own CA to sign certificates for {es}, set the -`elasticsearch.ssl.certificateAuthorities` setting in `kibana.yml` to specify -the location of the PEM file. -+ --- -[source,yaml] --------------------------------------------------------------------------------- -elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem --------------------------------------------------------------------------------- -Setting the `certificateAuthorities` property lets you use the default -`verificationMode` option of `full`. -//TBD: Is this still true? It isn't mentioned in https://www.elastic.co/guide/en/kibana/master/settings.html +Using the HTTPS protocol results in a default `elasticsearch.ssl.verificationMode` option of `full`, which utilizes hostname verification. For more information, see <>. -- -. (Optional) If the Elastic {monitor-features} are enabled, configure {kib} to -connect to the {es} monitoring cluster via HTTPS: +. Specify the {es} cluster's CA certificate chain in `kibana.yml`: + -- -NOTE: To perform this step, you must -{ref}/configuring-security.html[enable the {es} {security-features}] or you -must have a proxy that provides an HTTPS endpoint for {es}. --- +If you are using your own CA to sign certificates for {es}, then you need to specify the CA certificate chain in {kib} to properly establish +trust in TLS connections. If your CA certificate chain is contained in a PKCS #12 trust store, specify it like so: -.. Specify the HTTPS URL in the `xpack.monitoring.elasticsearch.hosts` setting in -the {kib} configuration file, `kibana.yml` -+ --- [source,yaml] -------------------------------------------------------------------------------- -xpack.monitoring.elasticsearch.hosts: ["https://:9200"] +elasticsearch.ssl.truststore.path: "/path/to/your/truststore.p12" +elasticsearch.ssl.truststore.password: "optional decryption password" -------------------------------------------------------------------------------- --- -.. Specify the `xpack.monitoring.elasticsearch.ssl.*` settings in the -`kibana.yml` file. -+ --- -For example, if you are using your own certificate authority to sign -certificates, specify the location of the PEM file in the `kibana.yml` file: +Otherwise, if your CA certificate chain is in PEM format, specify each certificate like so: [source,yaml] -------------------------------------------------------------------------------- -xpack.monitoring.elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem +elasticsearch.ssl.certificateAuthorities: ["/path/to/your/cacert1.pem", "/path/to/your/cacert2.pem"] -------------------------------------------------------------------------------- + -- + +. (Optional) If the Elastic {monitor-features} are enabled, configure {kib} to connect to the {es} monitoring cluster via HTTPS. The steps +are the same as above, but each setting is prefixed by `xpack.monitoring.`. For example, `xpack.monitoring.elasticsearch.hosts`, +`xpack.monitoring.elasticsearch.ssl.truststore.path`, etc. diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc index 110533589cab9..481cbc6e39418 100644 --- a/docs/visualize/visualize_rollup_data.asciidoc +++ b/docs/visualize/visualize_rollup_data.asciidoc @@ -6,7 +6,7 @@ beta[] You can visualize your rolled up data in a variety of charts, tables, maps, and more. Most visualizations support rolled up data, with the exception of -Timelion, TSVB, and Vega visualizations. +Timelion and Vega visualizations. To get started, go to *Management > Kibana > Index patterns.* If a rollup index is detected in the cluster, *Create index pattern* diff --git a/examples/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts index 23c82225563c8..653aa217717fa 100644 --- a/examples/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { DataPluginSetup } from 'src/plugins/data/server/plugin'; +import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; import { demoSearchStrategyProvider } from './demo_search_strategy'; import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common'; diff --git a/examples/state_containers_examples/kibana.json b/examples/state_containers_examples/kibana.json new file mode 100644 index 0000000000000..9114a414a4da3 --- /dev/null +++ b/examples/state_containers_examples/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "stateContainersExamples", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["state_containers_examples"], + "server": false, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/examples/state_containers_examples/package.json b/examples/state_containers_examples/package.json new file mode 100644 index 0000000000000..b309494a36662 --- /dev/null +++ b/examples/state_containers_examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "state_containers_examples", + "version": "1.0.0", + "main": "target/examples/state_containers_examples", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/examples/state_containers_examples/public/app.tsx b/examples/state_containers_examples/public/app.tsx new file mode 100644 index 0000000000000..319680d07f9bc --- /dev/null +++ b/examples/state_containers_examples/public/app.tsx @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AppMountParameters } from 'kibana/public'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import { createHashHistory, createBrowserHistory } from 'history'; +import { TodoAppPage } from './todo'; + +export interface AppOptions { + appInstanceId: string; + appTitle: string; + historyType: History; +} + +export enum History { + Browser, + Hash, +} + +export const renderApp = ( + { appBasePath, element }: AppMountParameters, + { appInstanceId, appTitle, historyType }: AppOptions +) => { + const history = + historyType === History.Browser + ? createBrowserHistory({ basename: appBasePath }) + : createHashHistory(); + ReactDOM.render( + { + const stripTrailingSlash = (path: string) => + path.charAt(path.length - 1) === '/' ? path.substr(0, path.length - 1) : path; + const currentAppUrl = stripTrailingSlash(history.createHref(history.location)); + if (historyType === History.Browser) { + // browser history + const basePath = stripTrailingSlash(appBasePath); + return currentAppUrl === basePath && !history.location.search && !history.location.hash; + } else { + // hashed history + return currentAppUrl === '#' && !history.location.search; + } + }} + />, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts b/examples/state_containers_examples/public/index.ts similarity index 86% rename from src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts rename to examples/state_containers_examples/public/index.ts index f6c6079822cb5..bc7ad78574ddb 100644 --- a/src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts +++ b/examples/state_containers_examples/public/index.ts @@ -17,5 +17,6 @@ * under the License. */ -export const mockReadFileSync = jest.fn(); -jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); +import { StateContainersExamplesPlugin } from './plugin'; + +export const plugin = () => new StateContainersExamplesPlugin(); diff --git a/examples/state_containers_examples/public/plugin.ts b/examples/state_containers_examples/public/plugin.ts new file mode 100644 index 0000000000000..beb7b93dbc5b6 --- /dev/null +++ b/examples/state_containers_examples/public/plugin.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. + */ + +import { AppMountParameters, CoreSetup, Plugin } from 'kibana/public'; + +export class StateContainersExamplesPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ + id: 'state-containers-example-browser-history', + title: 'State containers example - browser history routing', + async mount(params: AppMountParameters) { + const { renderApp, History } = await import('./app'); + return renderApp(params, { + appInstanceId: '1', + appTitle: 'Routing with browser history', + historyType: History.Browser, + }); + }, + }); + core.application.register({ + id: 'state-containers-example-hash-history', + title: 'State containers example - hash history routing', + async mount(params: AppMountParameters) { + const { renderApp, History } = await import('./app'); + return renderApp(params, { + appInstanceId: '2', + appTitle: 'Routing with hash history', + historyType: History.Hash, + }); + }, + }); + } + + public start() {} + public stop() {} +} diff --git a/examples/state_containers_examples/public/todo.tsx b/examples/state_containers_examples/public/todo.tsx new file mode 100644 index 0000000000000..84defb4a91e3f --- /dev/null +++ b/examples/state_containers_examples/public/todo.tsx @@ -0,0 +1,327 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { Link, Route, Router, Switch, useLocation } from 'react-router-dom'; +import { History } from 'history'; +import { + EuiButton, + EuiCheckbox, + EuiFieldText, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; +import { + BaseStateContainer, + INullableBaseStateContainer, + createKbnUrlStateStorage, + createSessionStorageStateStorage, + createStateContainer, + createStateContainerReactHelpers, + PureTransition, + syncStates, + getStateFromKbnUrl, +} from '../../../src/plugins/kibana_utils/public'; +import { useUrlTracker } from '../../../src/plugins/kibana_react/public'; +import { + defaultState, + pureTransitions, + TodoActions, + TodoState, +} from '../../../src/plugins/kibana_utils/demos/state_containers/todomvc'; + +interface GlobalState { + text: string; +} +interface GlobalStateAction { + setText: PureTransition; +} +const defaultGlobalState: GlobalState = { text: '' }; +const globalStateContainer = createStateContainer( + defaultGlobalState, + { + setText: state => text => ({ ...state, text }), + } +); + +const GlobalStateHelpers = createStateContainerReactHelpers(); + +const container = createStateContainer(defaultState, pureTransitions); +const { Provider, connect, useTransitions, useState } = createStateContainerReactHelpers< + typeof container +>(); + +interface TodoAppProps { + filter: 'completed' | 'not-completed' | null; +} + +const TodoApp: React.FC = ({ filter }) => { + const { setText } = GlobalStateHelpers.useTransitions(); + const { text } = GlobalStateHelpers.useState(); + const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions(); + const todos = useState(); + const filteredTodos = todos.filter(todo => { + if (!filter) return true; + if (filter === 'completed') return todo.completed; + if (filter === 'not-completed') return !todo.completed; + return true; + }); + const location = useLocation(); + return ( + <> +
+ + + All + + + + + Completed + + + + + Not Completed + + +
+
    + {filteredTodos.map(todo => ( +
  • + { + editTodo({ + ...todo, + completed: e.target.checked, + }); + }} + label={todo.text} + /> + { + deleteTodo(todo.id); + }} + > + Delete + +
  • + ))} +
+
{ + const inputRef = (e.target as HTMLFormElement).elements.namedItem( + 'newTodo' + ) as HTMLInputElement; + if (!inputRef || !inputRef.value) return; + addTodo({ + text: inputRef.value, + completed: false, + id: todos.map(todo => todo.id).reduce((a, b) => Math.max(a, b), 0) + 1, + }); + inputRef.value = ''; + e.preventDefault(); + }} + > + + +
+ + setText(e.target.value)} /> +
+ + ); +}; + +const TodoAppConnected = GlobalStateHelpers.connect(() => ({}))( + connect(() => ({}))(TodoApp) +); + +export const TodoAppPage: React.FC<{ + history: History; + appInstanceId: string; + appTitle: string; + appBasePath: string; + isInitialRoute: () => boolean; +}> = props => { + const initialAppUrl = React.useRef(window.location.href); + const [useHashedUrl, setUseHashedUrl] = React.useState(false); + + /** + * Replicates what src/legacy/ui/public/chrome/api/nav.ts did + * Persists the url in sessionStorage and tries to restore it on "componentDidMount" + */ + useUrlTracker(`lastUrlTracker:${props.appInstanceId}`, props.history, urlToRestore => { + // shouldRestoreUrl: + // App decides if it should restore url or not + // In this specific case, restore only if navigated to initial route + if (props.isInitialRoute()) { + // navigated to the base path, so should restore the url + return true; + } else { + // navigated to specific route, so should not restore the url + return false; + } + }); + + useEffect(() => { + // have to sync with history passed to react-router + // history v5 will be singleton and this will not be needed + const kbnUrlStateStorage = createKbnUrlStateStorage({ + useHash: useHashedUrl, + history: props.history, + }); + + const sessionStorageStateStorage = createSessionStorageStateStorage(); + + /** + * Restoring global state: + * State restoration similar to what GlobalState in legacy world did + * It restores state both from url and from session storage + */ + const globalStateKey = `_g`; + const globalStateFromInitialUrl = getStateFromKbnUrl( + globalStateKey, + initialAppUrl.current + ); + const globalStateFromCurrentUrl = kbnUrlStateStorage.get(globalStateKey); + const globalStateFromSessionStorage = sessionStorageStateStorage.get( + globalStateKey + ); + + const initialGlobalState: GlobalState = { + ...defaultGlobalState, + ...globalStateFromCurrentUrl, + ...globalStateFromSessionStorage, + ...globalStateFromInitialUrl, + }; + globalStateContainer.set(initialGlobalState); + kbnUrlStateStorage.set(globalStateKey, initialGlobalState, { replace: true }); + sessionStorageStateStorage.set(globalStateKey, initialGlobalState); + + /** + * Restoring app local state: + * State restoration similar to what AppState in legacy world did + * It restores state both from url + */ + const appStateKey = `_todo-${props.appInstanceId}`; + const initialAppState: TodoState = + getStateFromKbnUrl(appStateKey, initialAppUrl.current) || + kbnUrlStateStorage.get(appStateKey) || + defaultState; + container.set(initialAppState); + kbnUrlStateStorage.set(appStateKey, initialAppState, { replace: true }); + + // start syncing only when made sure, that state in synced + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: appStateKey, + stateStorage: kbnUrlStateStorage, + }, + { + stateContainer: withDefaultState(globalStateContainer, defaultGlobalState), + storageKey: globalStateKey, + stateStorage: kbnUrlStateStorage, + }, + { + stateContainer: withDefaultState(globalStateContainer, defaultGlobalState), + storageKey: globalStateKey, + stateStorage: sessionStorageStateStorage, + }, + ]); + + start(); + + return () => { + stop(); + + // reset state containers + container.set(defaultState); + globalStateContainer.set(defaultGlobalState); + }; + }, [props.appInstanceId, props.history, useHashedUrl]); + + return ( + + + + + + + +

+ State sync example. Instance: ${props.appInstanceId}. {props.appTitle} +

+
+ setUseHashedUrl(!useHashedUrl)}> + {useHashedUrl ? 'Use Expanded State' : 'Use Hashed State'} + +
+
+ + + + + + + + + + + + + + + +
+
+
+
+ ); +}; + +function withDefaultState( + stateContainer: BaseStateContainer, + // eslint-disable-next-line no-shadow + defaultState: State +): INullableBaseStateContainer { + return { + ...stateContainer, + set: (state: State | null) => { + if (Array.isArray(defaultState)) { + stateContainer.set(state || defaultState); + } else { + stateContainer.set({ + ...defaultState, + ...state, + }); + } + }, + }; +} diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json new file mode 100644 index 0000000000000..091130487791b --- /dev/null +++ b/examples/state_containers_examples/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [] +} diff --git a/package.json b/package.json index 99151f33962c4..0ed74dd65d1ab 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "packages": [ "packages/*", "x-pack", + "x-pack/plugins/*", "x-pack/legacy/plugins/*", "examples/*", "test/plugin_functional/plugins/*", @@ -114,7 +115,7 @@ "@babel/core": "^7.5.5", "@babel/register": "^7.7.0", "@elastic/apm-rum": "^4.6.0", - "@elastic/charts": "^16.0.2", + "@elastic/charts": "^16.1.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", "@elastic/eui": "17.3.1", @@ -133,8 +134,10 @@ "@kbn/pm": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", + "@kbn/ui-shared-deps": "1.0.0", "@types/json-stable-stringify": "^1.0.32", "@types/lodash.clonedeep": "^4.5.4", + "@types/node-forge": "^0.9.0", "@types/react-grid-layout": "^0.16.7", "@types/recompose": "^0.30.5", "JSONStream": "1.3.5", @@ -168,7 +171,6 @@ "elastic-apm-node": "^3.2.0", "elasticsearch": "^16.5.0", "elasticsearch-browser": "^16.5.0", - "encode-uri-query": "1.0.1", "execa": "^3.2.0", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", @@ -217,6 +219,7 @@ "mustache": "2.3.2", "ngreact": "0.5.1", "node-fetch": "1.7.3", + "node-forge": "^0.9.1", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", @@ -240,7 +243,7 @@ "react-use": "^13.13.0", "reactcss": "1.2.3", "redux": "4.0.0", - "redux-actions": "2.2.1", + "redux-actions": "2.6.5", "redux-thunk": "2.3.0", "regenerator-runtime": "^0.13.3", "regression": "2.0.1", @@ -314,7 +317,7 @@ "@types/delete-empty": "^2.0.0", "@types/elasticsearch": "^5.0.33", "@types/enzyme": "^3.9.0", - "@types/eslint": "^6.1.2", + "@types/eslint": "^6.1.3", "@types/fetch-mock": "^7.3.1", "@types/getopts": "^2.0.1", "@types/glob": "^7.1.1", @@ -341,6 +344,7 @@ "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", "@types/opn": "^5.1.0", + "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.3.2", "@types/podium": "^1.0.0", "@types/prop-types": "^15.5.3", @@ -353,7 +357,7 @@ "@types/react-router-dom": "^5.1.3", "@types/react-virtualized": "^9.18.7", "@types/redux": "^3.6.31", - "@types/redux-actions": "^2.2.1", + "@types/redux-actions": "^2.6.1", "@types/request": "^2.48.2", "@types/selenium-webdriver": "^4.0.5", "@types/semver": "^5.5.0", @@ -368,8 +372,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.12.0", - "@typescript-eslint/parser": "^2.12.0", + "@typescript-eslint/eslint-plugin": "^2.15.0", + "@typescript-eslint/parser": "^2.15.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", @@ -389,21 +393,21 @@ "enzyme-adapter-react-16": "^1.15.1", "enzyme-adapter-utils": "^1.12.1", "enzyme-to-json": "^3.4.3", - "eslint": "^6.5.1", - "eslint-config-prettier": "^6.4.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.9.0", "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-ban": "^1.3.0", - "eslint-plugin-cypress": "^2.7.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jest": "^22.19.0", + "eslint-plugin-ban": "^1.4.0", + "eslint-plugin-cypress": "^2.8.1", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-jest": "^23.3.0", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-mocha": "^6.2.0", + "eslint-plugin-mocha": "^6.2.2", "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-node": "^10.0.0", + "eslint-plugin-node": "^11.0.0", "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-react": "^7.16.0", - "eslint-plugin-react-hooks": "^2.1.2", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-react": "^7.17.0", + "eslint-plugin-react-hooks": "^2.3.0", "exit-hook": "^2.2.0", "faker": "1.1.0", "fetch-mock": "^7.3.9", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 04602d196a7f3..34b1b0fec376f 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,19 +15,19 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.12.0", - "@typescript-eslint/parser": "^2.12.0", + "@typescript-eslint/eslint-plugin": "^2.15.0", + "@typescript-eslint/parser": "^2.15.0", "babel-eslint": "^10.0.3", - "eslint": "^6.5.1", + "eslint": "^6.8.0", "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-ban": "^1.3.0", + "eslint-plugin-ban": "^1.4.0", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jest": "^22.19.0", - "eslint-plugin-mocha": "^6.2.0", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-jest": "^23.3.0", + "eslint-plugin-mocha": "^6.2.2", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.16.0", - "eslint-plugin-react-hooks": "^2.1.2" + "eslint-plugin-react": "^7.17.0", + "eslint-plugin-react-hooks": "^2.3.0" } } diff --git a/packages/kbn-analytics/src/report.ts b/packages/kbn-analytics/src/report.ts index 1c0b37966355f..16c0a3069e5fd 100644 --- a/packages/kbn-analytics/src/report.ts +++ b/packages/kbn-analytics/src/report.ts @@ -78,6 +78,7 @@ export class ReportManager { } assignReports(newMetrics: Metric | Metric[]) { wrapArray(newMetrics).forEach(newMetric => this.assignReport(this.report, newMetric)); + return { report: this.report }; } static createMetricKey(metric: Metric): string { switch (metric.type) { @@ -101,7 +102,7 @@ export class ReportManager { case METRIC_TYPE.USER_AGENT: { const { appName, type, userAgent } = metric; if (userAgent) { - this.report.userAgent = { + report.userAgent = { [key]: { key, appName, @@ -110,23 +111,22 @@ export class ReportManager { }, }; } + return; } case METRIC_TYPE.CLICK: case METRIC_TYPE.LOADED: case METRIC_TYPE.COUNT: { const { appName, type, eventName, count } = metric; - if (report.uiStatsMetrics) { - const existingStats = (report.uiStatsMetrics[key] || {}).stats; - this.report.uiStatsMetrics = this.report.uiStatsMetrics || {}; - this.report.uiStatsMetrics[key] = { - key, - appName, - eventName, - type, - stats: this.incrementStats(count, existingStats), - }; - } + report.uiStatsMetrics = report.uiStatsMetrics || {}; + const existingStats = (report.uiStatsMetrics[key] || {}).stats; + report.uiStatsMetrics[key] = { + key, + appName, + eventName, + type, + stats: this.incrementStats(count, existingStats), + }; return; } default: diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md index fd62f1b3c03b2..e6f3e60128983 100644 --- a/packages/kbn-config-schema/README.md +++ b/packages/kbn-config-schema/README.md @@ -156,6 +156,9 @@ __Usage:__ const valueSchema = schema.boolean({ defaultValue: false }); ``` +__Notes:__ +* The `schema.boolean()` also supports a string as input if it equals `'true'` or `'false'` (case-insensitive). + #### `schema.literal()` Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal. @@ -397,7 +400,7 @@ const valueSchema = schema.byteSize({ min: '3kb' }); ``` __Notes:__ -* The string value for `schema.byteSize()` and its options supports the following prefixes: `b`, `kb`, `mb`, `gb` and `tb`. +* The string value for `schema.byteSize()` and its options supports the following optional suffixes: `b`, `kb`, `mb`, `gb` and `tb`. The default suffix is `b`. * The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`. * Currently you cannot specify zero bytes with a string format and should use number `0` instead. @@ -417,7 +420,7 @@ const valueSchema = schema.duration({ defaultValue: '70ms' }); ``` __Notes:__ -* The string value for `schema.duration()` supports the following prefixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. +* The string value for `schema.duration()` supports the following optional suffixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. The default suffix is `ms`. * The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`. #### `schema.conditional()` diff --git a/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap b/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap index 1db6930062a9a..97e9082401b3d 100644 --- a/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap +++ b/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap @@ -1,9 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#constructor throws if number of bytes is negative 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-1024]"`; +exports[`#constructor throws if number of bytes is negative 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-1024]."`; -exports[`#constructor throws if number of bytes is not safe 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]"`; +exports[`#constructor throws if number of bytes is not safe 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]."`; -exports[`#constructor throws if number of bytes is not safe 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]"`; +exports[`#constructor throws if number of bytes is not safe 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]."`; -exports[`#constructor throws if number of bytes is not safe 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]"`; +exports[`#constructor throws if number of bytes is not safe 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]."`; + +exports[`parsing units throws an error when unsupported unit specified 1`] = `"Failed to parse [1tb] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`; diff --git a/packages/kbn-config-schema/src/byte_size_value/index.test.ts b/packages/kbn-config-schema/src/byte_size_value/index.test.ts index 46ed96c83dd1f..198d95aa0ab4c 100644 --- a/packages/kbn-config-schema/src/byte_size_value/index.test.ts +++ b/packages/kbn-config-schema/src/byte_size_value/index.test.ts @@ -20,6 +20,10 @@ import { ByteSizeValue } from '.'; describe('parsing units', () => { + test('number string (bytes)', () => { + expect(ByteSizeValue.parse('123').getValueInBytes()).toBe(123); + }); + test('bytes', () => { expect(ByteSizeValue.parse('123b').getValueInBytes()).toBe(123); }); @@ -37,12 +41,8 @@ describe('parsing units', () => { expect(ByteSizeValue.parse('1gb').getValueInBytes()).toBe(1073741824); }); - test('throws an error when no unit specified', () => { - expect(() => ByteSizeValue.parse('123')).toThrowError('could not parse byte size value'); - }); - test('throws an error when unsupported unit specified', () => { - expect(() => ByteSizeValue.parse('1tb')).toThrowError('could not parse byte size value'); + expect(() => ByteSizeValue.parse('1tb')).toThrowErrorMatchingSnapshot(); }); }); diff --git a/packages/kbn-config-schema/src/byte_size_value/index.ts b/packages/kbn-config-schema/src/byte_size_value/index.ts index fb0105503a149..48862821bb78d 100644 --- a/packages/kbn-config-schema/src/byte_size_value/index.ts +++ b/packages/kbn-config-schema/src/byte_size_value/index.ts @@ -35,9 +35,14 @@ export class ByteSizeValue { public static parse(text: string): ByteSizeValue { const match = /([1-9][0-9]*)(b|kb|mb|gb)/.exec(text); if (!match) { - throw new Error( - `could not parse byte size value [${text}]. Value must be a safe positive integer.` - ); + const number = Number(text); + if (typeof number !== 'number' || isNaN(number)) { + throw new Error( + `Failed to parse [${text}] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] ` + + `(e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer.` + ); + } + return new ByteSizeValue(number); } const value = parseInt(match[1], 0); @@ -49,8 +54,7 @@ export class ByteSizeValue { constructor(private readonly valueInBytes: number) { if (!Number.isSafeInteger(valueInBytes) || valueInBytes < 0) { throw new Error( - `Value in bytes is expected to be a safe positive integer, ` + - `but provided [${valueInBytes}]` + `Value in bytes is expected to be a safe positive integer, but provided [${valueInBytes}].` ); } } diff --git a/packages/kbn-config-schema/src/duration/index.ts b/packages/kbn-config-schema/src/duration/index.ts index ff8f96614a193..b96b5a3687bbb 100644 --- a/packages/kbn-config-schema/src/duration/index.ts +++ b/packages/kbn-config-schema/src/duration/index.ts @@ -25,10 +25,14 @@ const timeFormatRegex = /^(0|[1-9][0-9]*)(ms|s|m|h|d|w|M|Y)$/; function stringToDuration(text: string) { const result = timeFormatRegex.exec(text); if (!result) { - throw new Error( - `Failed to parse [${text}] as time value. ` + - `Format must be [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y')` - ); + const number = Number(text); + if (typeof number !== 'number' || isNaN(number)) { + throw new Error( + `Failed to parse [${text}] as time value. Value must be a duration in milliseconds, or follow the format ` + + `[ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer.` + ); + } + return numberToDuration(number); } const count = parseInt(result[1], 0); @@ -40,8 +44,7 @@ function stringToDuration(text: string) { function numberToDuration(numberMs: number) { if (!Number.isSafeInteger(numberMs) || numberMs < 0) { throw new Error( - `Failed to parse [${numberMs}] as time value. ` + - `Value should be a safe positive integer number.` + `Value in milliseconds is expected to be a safe positive integer, but provided [${numberMs}].` ); } diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts index 4d5091eaa09b1..044c3050f9fa8 100644 --- a/packages/kbn-config-schema/src/internals/index.ts +++ b/packages/kbn-config-schema/src/internals/index.ts @@ -82,7 +82,23 @@ export const internals = Joi.extend([ base: Joi.boolean(), coerce(value: any, state: State, options: ValidationOptions) { // If value isn't defined, let Joi handle default value if it's defined. - if (value !== undefined && typeof value !== 'boolean') { + if (value === undefined) { + return value; + } + + // Allow strings 'true' and 'false' to be coerced to booleans (case-insensitive). + + // From Joi docs on `Joi.boolean`: + // > Generates a schema object that matches a boolean data type. Can also + // > be called via bool(). If the validation convert option is on + // > (enabled by default), a string (either "true" or "false") will be + // converted to a boolean if specified. + if (typeof value === 'string') { + const normalized = value.toLowerCase(); + value = normalized === 'true' ? true : normalized === 'false' ? false : value; + } + + if (typeof value !== 'boolean') { return this.createError('boolean.base', { value }, state, options); } diff --git a/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap index c3f33dc29bf50..0e5f6de2deea8 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap @@ -9,3 +9,7 @@ exports[`returns error when not boolean 1`] = `"expected value of type [boolean] exports[`returns error when not boolean 2`] = `"expected value of type [boolean] but got [Array]"`; exports[`returns error when not boolean 3`] = `"expected value of type [boolean] but got [string]"`; + +exports[`returns error when not boolean 4`] = `"expected value of type [boolean] but got [number]"`; + +exports[`returns error when not boolean 5`] = `"expected value of type [boolean] but got [string]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap index f6f45a96ca161..ea2102b1776fb 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap @@ -18,6 +18,12 @@ ByteSizeValue { } `; +exports[`#defaultValue can be a string-formatted number 1`] = ` +ByteSizeValue { + "valueInBytes": 1024, +} +`; + exports[`#max returns error when larger 1`] = `"Value is [1mb] ([1048576b]) but it must be equal to or less than [1kb]"`; exports[`#max returns value when smaller 1`] = ` @@ -38,20 +44,18 @@ exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value o exports[`is required by default 1`] = `"expected value of type [ByteSize] but got [undefined]"`; -exports[`returns error when not string or positive safe integer 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-123]"`; +exports[`returns error when not valid string or positive safe integer 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-123]."`; -exports[`returns error when not string or positive safe integer 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]"`; +exports[`returns error when not valid string or positive safe integer 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]."`; -exports[`returns error when not string or positive safe integer 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]"`; +exports[`returns error when not valid string or positive safe integer 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]."`; -exports[`returns error when not string or positive safe integer 4`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]"`; +exports[`returns error when not valid string or positive safe integer 4`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]."`; -exports[`returns error when not string or positive safe integer 5`] = `"expected value of type [ByteSize] but got [Array]"`; +exports[`returns error when not valid string or positive safe integer 5`] = `"expected value of type [ByteSize] but got [Array]"`; -exports[`returns error when not string or positive safe integer 6`] = `"expected value of type [ByteSize] but got [RegExp]"`; +exports[`returns error when not valid string or positive safe integer 6`] = `"expected value of type [ByteSize] but got [RegExp]"`; -exports[`returns value by default 1`] = ` -ByteSizeValue { - "valueInBytes": 123, -} -`; +exports[`returns error when not valid string or positive safe integer 7`] = `"Failed to parse [123foo] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`; + +exports[`returns error when not valid string or positive safe integer 8`] = `"Failed to parse [123 456] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap index a21c28e7cc614..c4e4ff652a2d7 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap @@ -6,20 +6,24 @@ exports[`#defaultValue can be a number 1`] = `"PT0.6S"`; exports[`#defaultValue can be a string 1`] = `"PT1H"`; +exports[`#defaultValue can be a string-formatted number 1`] = `"PT0.6S"`; + exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [moment.Duration] but got [undefined]"`; exports[`is required by default 1`] = `"expected value of type [moment.Duration] but got [undefined]"`; -exports[`returns error when not string or non-safe positive integer 1`] = `"Failed to parse [-123] as time value. Value should be a safe positive integer number."`; +exports[`returns error when not valid string or non-safe positive integer 1`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [-123]."`; + +exports[`returns error when not valid string or non-safe positive integer 2`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [NaN]."`; -exports[`returns error when not string or non-safe positive integer 2`] = `"Failed to parse [NaN] as time value. Value should be a safe positive integer number."`; +exports[`returns error when not valid string or non-safe positive integer 3`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [Infinity]."`; -exports[`returns error when not string or non-safe positive integer 3`] = `"Failed to parse [Infinity] as time value. Value should be a safe positive integer number."`; +exports[`returns error when not valid string or non-safe positive integer 4`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [9007199254740992]."`; -exports[`returns error when not string or non-safe positive integer 4`] = `"Failed to parse [9007199254740992] as time value. Value should be a safe positive integer number."`; +exports[`returns error when not valid string or non-safe positive integer 5`] = `"expected value of type [moment.Duration] but got [Array]"`; -exports[`returns error when not string or non-safe positive integer 5`] = `"expected value of type [moment.Duration] but got [Array]"`; +exports[`returns error when not valid string or non-safe positive integer 6`] = `"expected value of type [moment.Duration] but got [RegExp]"`; -exports[`returns error when not string or non-safe positive integer 6`] = `"expected value of type [moment.Duration] but got [RegExp]"`; +exports[`returns error when not valid string or non-safe positive integer 7`] = `"Failed to parse [123foo] as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."`; -exports[`returns value by default 1`] = `"PT2M3S"`; +exports[`returns error when not valid string or non-safe positive integer 8`] = `"Failed to parse [123 456] as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."`; diff --git a/packages/kbn-config-schema/src/types/boolean_type.test.ts b/packages/kbn-config-schema/src/types/boolean_type.test.ts index d6e274f05e3ff..e94999b505437 100644 --- a/packages/kbn-config-schema/src/types/boolean_type.test.ts +++ b/packages/kbn-config-schema/src/types/boolean_type.test.ts @@ -23,6 +23,17 @@ test('returns value by default', () => { expect(schema.boolean().validate(true)).toBe(true); }); +test('handles boolean strings', () => { + expect(schema.boolean().validate('true')).toBe(true); + expect(schema.boolean().validate('TRUE')).toBe(true); + expect(schema.boolean().validate('True')).toBe(true); + expect(schema.boolean().validate('TrUe')).toBe(true); + expect(schema.boolean().validate('false')).toBe(false); + expect(schema.boolean().validate('FALSE')).toBe(false); + expect(schema.boolean().validate('False')).toBe(false); + expect(schema.boolean().validate('FaLse')).toBe(false); +}); + test('is required by default', () => { expect(() => schema.boolean().validate(undefined)).toThrowErrorMatchingSnapshot(); }); @@ -49,4 +60,8 @@ test('returns error when not boolean', () => { expect(() => schema.boolean().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); expect(() => schema.boolean().validate('abc')).toThrowErrorMatchingSnapshot(); + + expect(() => schema.boolean().validate(0)).toThrowErrorMatchingSnapshot(); + + expect(() => schema.boolean().validate('no')).toThrowErrorMatchingSnapshot(); }); diff --git a/packages/kbn-config-schema/src/types/byte_size_type.test.ts b/packages/kbn-config-schema/src/types/byte_size_type.test.ts index 67eae1e7c382a..7c65ec2945b49 100644 --- a/packages/kbn-config-schema/src/types/byte_size_type.test.ts +++ b/packages/kbn-config-schema/src/types/byte_size_type.test.ts @@ -23,7 +23,15 @@ import { ByteSizeValue } from '../byte_size_value'; const { byteSize } = schema; test('returns value by default', () => { - expect(byteSize().validate('123b')).toMatchSnapshot(); + expect(byteSize().validate('123b')).toEqual(new ByteSizeValue(123)); +}); + +test('handles numeric strings', () => { + expect(byteSize().validate('123')).toEqual(new ByteSizeValue(123)); +}); + +test('handles numbers', () => { + expect(byteSize().validate(123)).toEqual(new ByteSizeValue(123)); }); test('is required by default', () => { @@ -51,6 +59,14 @@ describe('#defaultValue', () => { ).toMatchSnapshot(); }); + test('can be a string-formatted number', () => { + expect( + byteSize({ + defaultValue: '1024', + }).validate(undefined) + ).toMatchSnapshot(); + }); + test('can be a number', () => { expect( byteSize({ @@ -88,7 +104,7 @@ describe('#max', () => { }); }); -test('returns error when not string or positive safe integer', () => { +test('returns error when not valid string or positive safe integer', () => { expect(() => byteSize().validate(-123)).toThrowErrorMatchingSnapshot(); expect(() => byteSize().validate(NaN)).toThrowErrorMatchingSnapshot(); @@ -100,4 +116,8 @@ test('returns error when not string or positive safe integer', () => { expect(() => byteSize().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); expect(() => byteSize().validate(/abc/)).toThrowErrorMatchingSnapshot(); + + expect(() => byteSize().validate('123foo')).toThrowErrorMatchingSnapshot(); + + expect(() => byteSize().validate('123 456')).toThrowErrorMatchingSnapshot(); }); diff --git a/packages/kbn-config-schema/src/types/duration_type.test.ts b/packages/kbn-config-schema/src/types/duration_type.test.ts index 39655d43d7b75..09e92ce727f2a 100644 --- a/packages/kbn-config-schema/src/types/duration_type.test.ts +++ b/packages/kbn-config-schema/src/types/duration_type.test.ts @@ -23,7 +23,15 @@ import { schema } from '..'; const { duration, object, contextRef, siblingRef } = schema; test('returns value by default', () => { - expect(duration().validate('123s')).toMatchSnapshot(); + expect(duration().validate('123s')).toEqual(momentDuration(123000)); +}); + +test('handles numeric string', () => { + expect(duration().validate('123000')).toEqual(momentDuration(123000)); +}); + +test('handles number', () => { + expect(duration().validate(123000)).toEqual(momentDuration(123000)); }); test('is required by default', () => { @@ -51,6 +59,14 @@ describe('#defaultValue', () => { ).toMatchSnapshot(); }); + test('can be a string-formatted number', () => { + expect( + duration({ + defaultValue: '600', + }).validate(undefined) + ).toMatchSnapshot(); + }); + test('can be a number', () => { expect( duration({ @@ -124,7 +140,7 @@ Object { }); }); -test('returns error when not string or non-safe positive integer', () => { +test('returns error when not valid string or non-safe positive integer', () => { expect(() => duration().validate(-123)).toThrowErrorMatchingSnapshot(); expect(() => duration().validate(NaN)).toThrowErrorMatchingSnapshot(); @@ -136,4 +152,8 @@ test('returns error when not string or non-safe positive integer', () => { expect(() => duration().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); expect(() => duration().validate(/abc/)).toThrowErrorMatchingSnapshot(); + + expect(() => duration().validate('123foo')).toThrowErrorMatchingSnapshot(); + + expect(() => duration().validate('123 456')).toThrowErrorMatchingSnapshot(); }); diff --git a/packages/kbn-dev-utils/certs/README.md b/packages/kbn-dev-utils/certs/README.md new file mode 100644 index 0000000000000..fdf7892789404 --- /dev/null +++ b/packages/kbn-dev-utils/certs/README.md @@ -0,0 +1,62 @@ +# Development certificates + +Kibana includes several development certificates to enable easy setup of TLS-encrypted communications with Elasticsearch. + +_Note: these certificates should **never** be used in production._ + +## Certificate information + +Certificates and keys are provided in multiple formats. These can be used by other packages to set up a new Elastic Stack with Kibana and Elasticsearch. The Certificate Authority (CA) private key is intentionally omitted from this package. + +### PEM + +* `ca.crt` -- A [PEM-formatted](https://tools.ietf.org/html/rfc1421) [X.509](https://tools.ietf.org/html/rfc5280) certificate that is used as a CA. +* `elasticsearch.crt` -- A PEM-formatted X.509 certificate and public key for Elasticsearch. +* `elasticsearch.key` -- A PEM-formatted [PKCS #1](https://tools.ietf.org/html/rfc8017) private key for Elasticsearch. +* `kibana.crt` -- A PEM-formatted X.509 certificate and public key for Kibana. +* `kibana.key` -- A PEM-formatted PKCS #1 private key for Kibana. + +### PKCS #12 + +* `elasticsearch.p12` -- A [PKCS #12](https://tools.ietf.org/html/rfc7292) encrypted key store / trust store that contains `ca.crt`, `elasticsearch.crt`, and a [PKCS #8](https://tools.ietf.org/html/rfc5208) encrypted version of `elasticsearch.key`. +* `kibana.p12` -- A PKCS #12 encrypted key store / trust store that contains `ca.crt`, `kibana.crt`, and a PKCS #8 encrypted version of `kibana.key`. + +The password used for both of these is "storepass". Other copies are also provided for testing purposes: + +* `elasticsearch_emptypassword.p12` -- The same PKCS #12 key store, encrypted with an empty password. +* `elasticsearch_nopassword.p12` -- The same PKCS #12 key store, not encrypted with a password. + +## Certificate generation + +[Elasticsearch cert-util](https://www.elastic.co/guide/en/elasticsearch/reference/current/certutil.html) and [OpenSSL](https://www.openssl.org/) were used to generate these certificates. The following commands were used from the root directory of Elasticsearch: + +``` +# Generate the PKCS #12 keystore for a CA, valid for 50 years +bin/elasticsearch-certutil ca -days 18250 --pass castorepass + +# Generate the PKCS #12 keystore for Elasticsearch and sign it with the CA +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name elasticsearch --dns localhost --pass storepass + +# Generate the PKCS #12 keystore for Kibana and sign it with the CA +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name kibana --dns localhost --pass storepass + +# Copy the PKCS #12 keystore for Elasticsearch with an empty password +openssl pkcs12 -in elasticsearch.p12 -nodes -passin pass:"storepass" -passout pass:"" | openssl pkcs12 -export -out elasticsearch_emptypassword.p12 -passout pass:"" + +# Manually create "elasticsearch_nopassword.p12" -- this can be done on macOS by importing the P12 key store into the Keychain and exporting it again + +# Extract the PEM-formatted X.509 certificate for the CA +openssl pkcs12 -in elasticsearch.p12 -out ca.crt -cacerts -passin pass:"storepass" -passout pass: + +# Extract the PEM-formatted PKCS #1 private key for Elasticsearch +openssl pkcs12 -in elasticsearch.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:keypass -out elasticsearch.key + +# Extract the PEM-formatted X.509 certificate for Elasticsearch +openssl pkcs12 -in elasticsearch.p12 -out elasticsearch.crt -clcerts -passin pass:"storepass" -passout pass: + +# Extract the PEM-formatted PKCS #1 private key for Kibana +openssl pkcs12 -in kibana.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:keypass -out kibana.key + +# Extract the PEM-formatted X.509 certificate for Kibana +openssl pkcs12 -in kibana.p12 -out kibana.crt -clcerts -passin pass:"storepass" -passout pass: +``` diff --git a/packages/kbn-dev-utils/certs/ca.crt b/packages/kbn-dev-utils/certs/ca.crt old mode 100755 new mode 100644 index 3e964823c5086..217935b8d83f6 --- a/packages/kbn-dev-utils/certs/ca.crt +++ b/packages/kbn-dev-utils/certs/ca.crt @@ -1,20 +1,29 @@ +Bag Attributes + friendlyName: elasticsearch + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37 +Key Attributes: +Bag Attributes + friendlyName: ca + 2.16.840.1.113894.746875.1.1: +subject=/CN=Elastic Certificate Tool Autogenerated CA +issuer=/CN=Elastic Certificate Tool Autogenerated CA -----BEGIN CERTIFICATE----- -MIIDSjCCAjKgAwIBAgIVAOgxLlE1RMGl2fYgTKDznvDL2vboMA0GCSqGSIb3DQEB -CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu -ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzQ0OFoXDTIyMDcxMDE3MzQ0OFowNDEyMDAG -A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNImKp/A9l++Ac7U5lvHOA -+fYRb8p7AgdfKBMB0v3bo+bpHjkbkf3vYHjo1xJSg5ls6EPK+Do4owkAgKJdrznI -5/efJOjgA+ylH4rgAfrRIQmiFEWZnAv86vJ+Iq83mfkPELb4dvXCi7AFQkzoM/rY -Lbi97xha5bA2SEmpYp7VhBTM9zWy+q9Tm5odPO8u2n75GpIM2RwipaXlL0ink+06 -/oweQJoivaDgpDOmUXCFPmpV3VCdhUGxDQPyG0upQkF+NbQoei4RmluPEmVz4S7I -TFLWjX7LeZVP63bJkcCgiq6Hm97kDtr9EYlPKhHm7UMWzhNzHbfvySMDzqAJC0KX -AgMBAAGjUzBRMB0GA1UdDgQWBBRKqaaQ/+jT+ipPLJe7qekp1N/zizAfBgNVHSME -GDAWgBRKqaaQ/+jT+ipPLJe7qekp1N/zizAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBCwUAA4IBAQA7Gcq8h8yDXvepfKUAcTTMCBZkI+g3qE1gfRwjW7587CIj -xnrzEqANU+Q1lv7IeQ158HiduDUMZfnvpuNwkf0HkqnRWb57RwfVdCAlAeZmzipq -5ZJWlIW4dbmk57nGLg4fCszedi0uSGytZ2/BUdpWyC0fAM97h7Agtr4xGGKMEL67 -uB55ijt61V62HZ5wWXWNO9m+wfmdnt+YQViQJHtpYz1oOmWhY3dpitZLfWs1sLLD -w3CZOhmWX7+P7+HlCkSBF4swzHOCI3THyX61NbLxju8VkTAjwbZPq4EOnVKnO6kr -RdwQVnzKnqG5fxfSGknNahy0pOhJHZlGLwECRlgF +MIIDSzCCAjOgAwIBAgIUW0brhEtYK3tUBYlXnUa+AMmAX6kwDQYJKoZIhvcNAQEL +BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l +cmF0ZWQgQ0EwIBcNMTkxMjI3MTcwMjMyWhgPMjA2OTEyMTQxNzAyMzJaMDQxMjAw +BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAplO5m5Xy8xERyA0/G5SM +Nu2QXkfS+m7ZTFjSmtwqX7BI1I6ISI4Yw8QxzcIgSbEGlSqb7baeT+A/1JQj0gZN +KOnKbazl+ujVRJpsfpt5iUsnQyVPheGekcHkB+9WkZPgZ1oGRENr/4Eb1VImQf+Y +yo/FUj8X939tYW0fficAqYKv8/4NWpBUbeop8wsBtkz738QKlmPkMwC4FbuF2/bN +vNuzQuRbGMVmPeyivZJRfDAMKExoXjCCLmbShdg4dUHsUjVeWQZ6s4vbims+8qF9 +b4bseayScQNNU3hc5mkfhEhSM0KB0lDpSvoCxuXvXzb6bOk7xIdYo+O4vHUhvSkQ +mwIDAQABo1MwUTAdBgNVHQ4EFgQUGu0mDnvDRnBdNBG8DxwPdWArB0kwHwYDVR0j +BBgwFoAUGu0mDnvDRnBdNBG8DxwPdWArB0kwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEASv/FYOwWGnQreH8ulcVupGeZj25dIjZiuKfJmslH8QN/ +pVCIzAxNZjGjCpKxbJoCu5U9USaBylbhigeBJEq4wmYTs/WPu4uYMgDj0MILuHin +RQqgEVG0uADGEgH2nnk8DeY8gQvGpJRQGlXNK8pb+pCsy6F8k/svGOeBND9osHfU +CVEo5nXjfq6JCFt6hPx7kl4h3/j3C4wNy/Dv/QINdpPsl6CnF17Q9R9d60WFv42/ +pkl7W1hszCG9foNJOJabuWfVoPkvKQjoCvPitZt/hCaFZAW49PmAVhK+DAohQ91l +TZhDmYqHoXNiRDQiUT68OS7RlfKgNpr/vMTZXDxpmw== -----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/certs/elasticsearch.crt b/packages/kbn-dev-utils/certs/elasticsearch.crt old mode 100755 new mode 100644 index b30e11e9bbce1..87ba02019903f --- a/packages/kbn-dev-utils/certs/elasticsearch.crt +++ b/packages/kbn-dev-utils/certs/elasticsearch.crt @@ -1,20 +1,29 @@ +Bag Attributes + friendlyName: elasticsearch + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37 +Key Attributes: +Bag Attributes + friendlyName: elasticsearch + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37 +subject=/CN=elasticsearch +issuer=/CN=Elastic Certificate Tool Autogenerated CA -----BEGIN CERTIFICATE----- -MIIDRDCCAiygAwIBAgIVAI8V1fwvXKykKtp5k0cLpTOtY+DVMA0GCSqGSIb3DQEB +MIIDQDCCAiigAwIBAgIVAI93OQE6tZffPyzenSg3ljE3JJBzMA0GCSqGSIb3DQEB CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu -ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzUxOFoXDTIyMDcxMDE3MzUxOFowGDEWMBQG -A1UEAxMNZWxhc3RpY3NlYXJjaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALW+8gV6m6wYmTZmrXzNWKElE+ePkkikCviNfuWonWqxgAoWpAwAx2FvdhP3 -UDFbe38ydJX4oDgXeC25vdIR6z2uqzx+GXSNSybO7luuOUYQOP4Xf5Cj3zzXXMyu -nY1nZTVsChI9jAMz4cZZdUd04f4r4TBNxrFCcVR0uec5RGRXuP8rSQd9AbYFUVYf -jJeLb24asghb2Ku+c2JGvMqPEXFWFGOXFhUoIbRjCJNTDcr1ZXPof3+fO1l6HmhT -QBSqC4IZL8XqANltDT4tCQDD8L9+ckWJD8MP3wPkPUGZId2gLu++hrb9YfiP2upq -N/f3P7l5Fcisw1iwQC4+DGMTyfcCAwEAAaNpMGcwHQYDVR0OBBYEFGuiGk8HLpG2 -MyA24/J+GwxT32ikMB8GA1UdIwQYMBaAFEqpppD/6NP6Kk8sl7up6SnU3/OLMBoG -A1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB -CwUAA4IBAQB8yfY0edAgq2KnJNWyl8NpHNfqtM27+/LR2V8OxVwxV1hc4ZilczLu -CXeqP9uqBVjcck6fvLrjy4LhSG0V05j51UMJ1FjFVTBuhlrDcd3j8848yWrmyz8z -vPYYY2vIN9d1NsBgufULwliBT4UJchsYE8xT5ayAzGHKCTlzHGHMTPzYjwac8nbT -nd2u+6h0OQOJn6K4v+RfXtN4EA8ZUrYxUkqHNS3cFB5sxH7JQGi25XJc5MfxyCwY -YOukxbN85ew861N6oVd+W+nGJu8WOLU88/uvCv+dLhnAlnnIOLqvmrD5m7gFsFO9 -Z7Xz/U1SbNipWy9OLOhqq2Ja59j8p9e5 +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDMxN1oYDzIwNjkxMjE0MTcwMzE3WjAYMRYw +FAYDVQQDEw1lbGFzdGljc2VhcmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2EkPfvE3ZNMjHCAQZhpImoXBCIN6KavvJSbVHRtLzAXB4wxige+vFQWb +4umqPeEeVH7FvrsRqn24tUgGIkag9p9AOwYxfcT3vwNqcK/EztIlYFs72pmYg7Ez +s6+qLc/YSLOT3aMoHKDHE93z1jYIDGccyjGbv9NsdgCbLHD0TQuqm+7pKy1MZoJm +0qn4KYw4kXakVNWlxm5GIwr8uqU/w4phrikcOOWqRzsxByoQajypLOA4eD/uWnI2 +zGyPQy7Bkxojiy1ss0CVlrl8fJgcjC4PONpm1ibUSX3SoZ8PopPThR6gvvwoQolR +rYu4+D+rsX7q/ldA6vBOiHBD8r4QoQIDAQABo2MwYTAdBgNVHQ4EFgQUSlIMCYYd +e72A0rUqaCkjVPkGPIwwHwYDVR0jBBgwFoAUGu0mDnvDRnBdNBG8DxwPdWArB0kw +FAYDVR0RBA0wC4IJbG9jYWxob3N0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQAD +ggEBAImbzBVAEjiLRsNDLP7QAl0k7lVmfQRFz5G95ZTAUSUgbqqymDvry47yInFF +3o12TuI1GxK5zHzi+qzpJLyrnGwGK5JBR+VGxIBBKFVcFh1WNGKV6kSO/zBzO7PO +4Jw4G7By/ImWvS0RBhBUQ9XbQZN3WcVkVVV8UQw5Y7JoKtM+fzyEKXKRCTsvgH+h +3+fUBgqwal2Mz4KPH57Jrtk209dtn7tnQxHTNLo0niHyEcfrpuG3YFqTwekr+5FF +FniIcYHPGjag1WzLIdyhe88FFpuav19mlCaxBACc7t97v+euSVUWnsKpy4dLydpv +NxJiI9eWbJZ7f5VM7o64pm7U1cU= -----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/certs/elasticsearch.key b/packages/kbn-dev-utils/certs/elasticsearch.key old mode 100755 new mode 100644 index 1013ce3971246..9ae4e314630d1 --- a/packages/kbn-dev-utils/certs/elasticsearch.key +++ b/packages/kbn-dev-utils/certs/elasticsearch.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAtb7yBXqbrBiZNmatfM1YoSUT54+SSKQK+I1+5aidarGAChak -DADHYW92E/dQMVt7fzJ0lfigOBd4Lbm90hHrPa6rPH4ZdI1LJs7uW645RhA4/hd/ -kKPfPNdczK6djWdlNWwKEj2MAzPhxll1R3Th/ivhME3GsUJxVHS55zlEZFe4/ytJ -B30BtgVRVh+Ml4tvbhqyCFvYq75zYka8yo8RcVYUY5cWFSghtGMIk1MNyvVlc+h/ -f587WXoeaFNAFKoLghkvxeoA2W0NPi0JAMPwv35yRYkPww/fA+Q9QZkh3aAu776G -tv1h+I/a6mo39/c/uXkVyKzDWLBALj4MYxPJ9wIDAQABAoIBAQCb1ggrjn/gxo7I -yK3FL0XplqNEkCR8SLxndtvyC+w+Schh3hv3dst+zlXOtOZ8C9cOr7KrzS2EKwuP -GY6bi2XL0/NbwTwOZgCkXBahYfgWDV7w8DEfUoPd5UPa9XZ+gsOTVPolvcRKErhq -nNYk2SHWEMXb5zSRVUlbg2LL0pzD88bIuKJX+FwPvWcQc2P4OdVTq77iedcl82zZ -6PqTNqKMep7/odLQeBfX7OapOAviVnPYHe0TA114COOimR/pK8IA1OJymX5rgU7O -Wh+uNBSxdHsTTYTkAvw8Bt5Q8n1WCpQwZoYU3xWuSlu7eJ7kcgdFOu9r9GjSXysT -UYCd8s0BAoGBAPXPpCDRxjqF3/ToZ5x5dorKxxJyrmldzMJaUjqOv7y6kezbdBql -n7p3AJ5UfYUW/N6pgQXaWF4MPSyj7ItHhwHjL+v0Manmi5gq8oA30fplhjUlPre7 -Lx4v7SEmH739EHrkZ2ClIQwY3wKuN8mZKgw6RseFgphczDmhHCqEbjW3AoGBAL1H -fkl0RNdZ3nZg0u7MUVk8ytnqBsp7bNFhEs0zUl7ghu3NLaPt8qhirG638oMSCxqH -FPeM3/DryokQAym+UHYNMwiBziEUB2CKMMj7S5YFFWIldCxFeImCO2EP+y3hmbTZ -yjsznNrDzQtErZGP+JTRZcy9xF0oAfVt0G/O1Q3BAoGAa8bqINW5g6l1Q82uuEXt -evdkB6uu21YcVE8D5Nb4LMjk+KRUKObbvQc2hzVmf7dPklVh0+4jdsEJBYyuR3dK -M8KoHV3JdMQ4CrUx9JQFBjQDf0PgVvDEvQiogTNVEZlm42tIBHECp2o0RdmbblIw -xIG8zPi2BRYTGWWRkvbT18sCgYA+c/B/XBW62LRGavwuPsw4nY5xCH7lIIRvMZB6 -lIyBMaRToneEt2ZxmN08SwWBqdpwDlIkvB7H54UUZGwmwdzaltBX5jyVPX6RpAck -yYXPIi5EDAeg8+sptAbTp+pA4UdOHO5VSlpe9GwbY7XBabejotPsElFQS3sZ9/nm -amByAQKBgQCJWghllys1qk76/6PmeVjwjaK9n8o+94LWhqODXlACmDRyse5dwpYb -BIsMMZrNu1YsqDXlWpU7xNa6A8j4oa+EPnm/01PjdueAvMB/oE1woawM5tSsd8NQ -zeQPDhxjDxzaO5l4oJLZg6FT7iQAprhYZjgb8m1vz0D2Xid0A3Kgpw== +MIIEogIBAAKCAQEA2EkPfvE3ZNMjHCAQZhpImoXBCIN6KavvJSbVHRtLzAXB4wxi +ge+vFQWb4umqPeEeVH7FvrsRqn24tUgGIkag9p9AOwYxfcT3vwNqcK/EztIlYFs7 +2pmYg7Ezs6+qLc/YSLOT3aMoHKDHE93z1jYIDGccyjGbv9NsdgCbLHD0TQuqm+7p +Ky1MZoJm0qn4KYw4kXakVNWlxm5GIwr8uqU/w4phrikcOOWqRzsxByoQajypLOA4 +eD/uWnI2zGyPQy7Bkxojiy1ss0CVlrl8fJgcjC4PONpm1ibUSX3SoZ8PopPThR6g +vvwoQolRrYu4+D+rsX7q/ldA6vBOiHBD8r4QoQIDAQABAoIBAB+s44YV0aUEfvnZ +gE1TwBpRSGn0x2le8tEgFMoEe19P4Itd/vdEoQGVJrVevz38wDJjtpYuU3ICo5B5 +EdznNx+nRwLd71WaCSaCW45RT6Nyh2LLOcLUB9ARnZ7NNUEsVWKgWiF1iaRXr5Ar +S1Ct7RPT7hV2mnbHgfTuNcuWZ1D5BUcqNczNoHsV6guFChiwTr7ZObnKj4qJLwdu +ioYYWno4ZLgsk4SfW6DXUCvfKROfYdDd2rGu0NQ4QxT3Q98AsXlrlUITBQbpQEgy +5GSTEh/4sRYj4NQZqncDpPgXm22kYdU7voBjt/zu66oq1W6kKQ4JwPmyc2SI0haa +/pyCMtkCgYEA/y3vs59RvrM6xpT77lf7WigSBbIBQxeKs9RGNoN0Nn/eR0MlQAUG +SmCkkEOcUGuVMnoo5Kc73IP/Q1+O4UGg7f1Gs8KeFPFQMm/wcSL7obvRWray1Bw6 +ohITJPqZYZrw3hmkOMxkLpvUydivN1Unm7BezjOa+T/+OaV3PyAYufsCgYEA2Psb +S8OQhFiVbOKlMYOebvG+AnhAzJiSVus9R9NcViv20E61PRj2rfA398pYpZ8nxaQp +cWGy+POZbkxRCprZ1GHkwWjaQysgeOCbJv8nQ2oh5C0ZCaGw6lfmi2mN097+Prmx +QE8j8OKj3wVI6bniCF7vzwfG3c5cU73elLTAWRMCgYBoA/eDRlvx2ekJbU1MGDzy +wQann6l4Ca6WIt8D9Y13caPPdIVIlUO9KauqyoR7G39TdgwZODnkZ0Gz2s3I8BGD +MQyS1a/OZZcFGC/wTgw4HvD1gydd4qvbyHZZSnUfHiM0xUr1hAsKHKceJ980NNfS +VJAwiUSQeQ9NvC7hYlnx5QKBgDxESsmZcRuBa0eKEC4Xi7rvBEK1WfI58nOX9TZs ++3mnzm7/XZGxzFp1nWYC2uptsWNQ/H3UkBxbtOMQ6XWTmytFYX9i+zSq1uMcJ5wG +RMaRxQYWjJzDP1tnvM4+LDmL93w+oX/mO2pd2PxKAH2CtshybhNH6rGS7swHsboG +FmLnAoGAYTnTcWD1qiwjbJR5ZdukAjIq39cGcf0YOVJCiaFS+5vTirbw04ARvNyM +rxU8EpVN1sKC411pgNvlm6KZJHwihRRQoY+UI2fn78bHBH991QhlrTPO6TBZx7Aw ++hzyxqAiSBX65dQo0e4C15wZysQO/bdT5Def0+UTDR8j8ZgMAQg= -----END RSA PRIVATE KEY----- diff --git a/packages/kbn-dev-utils/certs/elasticsearch.p12 b/packages/kbn-dev-utils/certs/elasticsearch.p12 new file mode 100644 index 0000000000000..02a9183cd8a50 Binary files /dev/null and b/packages/kbn-dev-utils/certs/elasticsearch.p12 differ diff --git a/packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12 b/packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12 new file mode 100644 index 0000000000000..3162982ac635a Binary files /dev/null and b/packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12 differ diff --git a/packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12 b/packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12 new file mode 100644 index 0000000000000..3a22a58d207df Binary files /dev/null and b/packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12 differ diff --git a/packages/kbn-dev-utils/certs/kibana.crt b/packages/kbn-dev-utils/certs/kibana.crt new file mode 100644 index 0000000000000..1c83be587bff9 --- /dev/null +++ b/packages/kbn-dev-utils/certs/kibana.crt @@ -0,0 +1,29 @@ +Bag Attributes + friendlyName: kibana + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 32 32 33 30 33 39 +Key Attributes: +Bag Attributes + friendlyName: kibana + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 32 32 33 30 33 39 +subject=/CN=kibana +issuer=/CN=Elastic Certificate Tool Autogenerated CA +-----BEGIN CERTIFICATE----- +MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== +-----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/certs/kibana.key b/packages/kbn-dev-utils/certs/kibana.key new file mode 100644 index 0000000000000..4a4e6b4cb8c36 --- /dev/null +++ b/packages/kbn-dev-utils/certs/kibana.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAkMGGG0LW0QSieLjWXNviIEVPuzTS9tWXm0JRCDICu4wKjjK7 +Ftm+QwE8+NRXfhkBVBVGGI8KPj0I+rCR7ITiaP4DW99h68nbzCO2D73r5LM5LhBg +n18UNw8NkUwAi4A9EDgpMB7590zH9JaJaFf3jxlmmQ2GWl4t2uQ3VovxxRaCcrIB +dCVdn+l8ioSZBmhHybPaVl9uvcx/10LxjceARIpiaWAfIeCZw2DxSIkHao/rJdm/ +pEtYR/ZC+YAQDF4xVL3DZ1/j5sk48iaxqO+ZNJLvrKQX9WzMLkLd7kpjPk0CF5Lx +Q5ktAzlREq6QORAtUwVBkc+qPyPPF8eDLne+9QIDAQABAoIBAHl9suxWYKz00te3 +alJtSZAEHDLm1tjL034/XnseXiTCGGnYMiWvgnwCIgZFUVlH61GCuV4LT3GFEHA2 +mYKE1PGBn5gQF8MpnAvtPPRhVgaQVUFQBYg86F59h8mWnC545sciG4+DsA/apUem +wJSOn/u+Odni/AwEV0ALolZFBhl+0rccSr+6paJnzJ7QNiIn6EWbgb0n9WXqkhap +TqoPclBHm0ObeBI6lNyfvBZ8HB3hyjWZInNCaAs9DnkNPh4evuttUn/KlOPOVn9r +xz2UYsmVW6E+yPXUpSYkFQN9aaPF6alOz8PIfF8Wit7pmZMmInluGcwi/us9+ZTN +8gNvpoECgYEA0KC7XEoXRsBTN4kPznkGftvj1dtgB35W/HxXNouArQQjCbLhqcsA +jqaK0f+stYzSWZXGsKl9yQU9KA7u/wCHmLep70l7WsYYUKdkhWouK0HU5MeeLwB0 +N4ekQOQuQGqelqMo7IG2hQhTYD9PB4F3G0Sz1FgdObfuGPKfvNFVjckCgYEAsaAA +IY/TpRBWeWZfyXrnkp3atOPzkdpjb6cfT8Kib9bIECXr7ULUxA5QANX05ofodhsW +3+7iW5wicyZ1VNVEsPRL0aw7YUbNpBvob8faBUZ2KEdKQr42IfVOo7TQnvVXtumR +UE+dNvWUL2PbL0wMxD1XbMSmOze/wF8X2CeyDc0CgYBQnLqol2xVBz1gaRJ1emgb +HoXzfVemrZeY6cadKdwnfkC3n6n4fJsTg6CCMiOe5vHkca4bVvJmeSK/Vr3cRG0g +gl8kOaVzVrXQfE2oC3YZes9zMvqZOLivODcsZ77DXy82D4dhk2FeF/B3cR7tTIYk +QDCoLP/l7H8QnrdAMza2mQKBgDODwuX475ncviehUEB/26+DBo4V2ms/mj0kjAk2 +2qNy+DzuspjyHADsYbmMU+WUHxA51Q2HG7ET/E3HJpo+7BgiEecye1pADZ391hCt +Nob3I4eU/W2T+uEoYvFJnIOthg3veYyAOolY+ewwmr4B4WX8oGFUOx3Lklo5ehHf +mV01AoGBAI/c6OoHdcqQsZxKlxDNLyB2bTbowAcccoZIOjkC5fkkbsmMDLfScBfW +Q4YYJsmJBdrWNvo7jCl17Mcc4Is3RlmHDrItRkaZj+ehqAN3ejrnPLdgYeW/5XDK +e7yBj7oJd4oKZc59jVytdHvo5R8K0QohAv9gQEZ/tdypX+xWe+5E +-----END RSA PRIVATE KEY----- diff --git a/packages/kbn-dev-utils/certs/kibana.p12 b/packages/kbn-dev-utils/certs/kibana.p12 new file mode 100644 index 0000000000000..06bbd23881290 Binary files /dev/null and b/packages/kbn-dev-utils/certs/kibana.p12 differ diff --git a/packages/kbn-dev-utils/src/certs.ts b/packages/kbn-dev-utils/src/certs.ts index 0d340e4e8c906..f72e3ee547b5c 100644 --- a/packages/kbn-dev-utils/src/certs.ts +++ b/packages/kbn-dev-utils/src/certs.ts @@ -22,3 +22,14 @@ import { resolve } from 'path'; export const CA_CERT_PATH = resolve(__dirname, '../certs/ca.crt'); export const ES_KEY_PATH = resolve(__dirname, '../certs/elasticsearch.key'); export const ES_CERT_PATH = resolve(__dirname, '../certs/elasticsearch.crt'); +export const ES_P12_PATH = resolve(__dirname, '../certs/elasticsearch.p12'); +export const ES_P12_PASSWORD = 'storepass'; +export const ES_EMPTYPASSWORD_P12_PATH = resolve( + __dirname, + '../certs/elasticsearch_emptypassword.p12' +); +export const ES_NOPASSWORD_P12_PATH = resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); +export const KBN_KEY_PATH = resolve(__dirname, '../certs/kibana.key'); +export const KBN_CERT_PATH = resolve(__dirname, '../certs/kibana.crt'); +export const KBN_P12_PATH = resolve(__dirname, '../certs/kibana.p12'); +export const KBN_P12_PASSWORD = 'storepass'; diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index dc12613cf2a9e..2fc29b71b262e 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -25,7 +25,19 @@ export { ToolingLogCollectingWriter, } from './tooling_log'; export { createAbsolutePathSerializer } from './serializers'; -export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; +export { + CA_CERT_PATH, + ES_KEY_PATH, + ES_CERT_PATH, + ES_P12_PATH, + ES_P12_PASSWORD, + ES_EMPTYPASSWORD_P12_PATH, + ES_NOPASSWORD_P12_PATH, + KBN_KEY_PATH, + KBN_CERT_PATH, + KBN_P12_PATH, + KBN_P12_PASSWORD, +} from './certs'; export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './repo_root'; export { KbnClient } from './kbn_client'; diff --git a/packages/kbn-dev-utils/src/run/README.md b/packages/kbn-dev-utils/src/run/README.md index 913b601058f87..99893a6237668 100644 --- a/packages/kbn-dev-utils/src/run/README.md +++ b/packages/kbn-dev-utils/src/run/README.md @@ -117,7 +117,7 @@ $ node scripts/my_task - *`flags.allowUnexpected: boolean`* - By default, any flag that is passed but not mentioned in `flags.string`, `flags.boolean`, `flags.alias` or `flags.default` will trigger an error, preventing the run function from calling its first argument. If you have a reason to disable this behavior set this option to `true`. + By default, any flag that is passed but not mentioned in `flags.string`, `flags.boolean`, `flags.alias` or `flags.default` will trigger an error, preventing the run function from calling its first argument. If you have a reason to disable this behavior set this option to `true`. Unexpected flags will be collected from argv into `flags.unexpected`. To parse these flags and guess at their types, you can additionally pass `flags.guessTypesForUnexpectedFlags` but that's not recommended. - ***`createFailError(reason: string, options: { exitCode: number, showHelp: boolean }): FailError`*** diff --git a/packages/kbn-dev-utils/src/run/flags.test.ts b/packages/kbn-dev-utils/src/run/flags.test.ts new file mode 100644 index 0000000000000..c730067a84f46 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/flags.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { getFlags } from './flags'; + +it('gets flags correctly', () => { + expect( + getFlags(['-a', '--abc=bcd', '--foo=bar', '--no-bar', '--foo=baz', '--box', 'yes', '-zxy'], { + flags: { + boolean: ['x'], + string: ['abc'], + alias: { + x: 'extra', + }, + allowUnexpected: true, + }, + }) + ).toMatchInlineSnapshot(` + Object { + "_": Array [], + "abc": "bcd", + "debug": false, + "extra": true, + "help": false, + "quiet": false, + "silent": false, + "unexpected": Array [ + "-a", + "--foo=bar", + "--foo=baz", + "--no-bar", + "--box", + "yes", + "-z", + "-y", + ], + "v": false, + "verbose": false, + "x": true, + } + `); +}); + +it('guesses types for unexpected flags', () => { + expect( + getFlags(['-abc', '--abc=bcd', '--no-foo', '--bar'], { + flags: { + allowUnexpected: true, + guessTypesForUnexpectedFlags: true, + }, + }) + ).toMatchInlineSnapshot(` + Object { + "_": Array [], + "a": true, + "abc": "bcd", + "b": true, + "bar": true, + "c": true, + "debug": false, + "foo": false, + "help": false, + "quiet": false, + "silent": false, + "unexpected": Array [ + "-a", + "-b", + "-c", + "-abc", + "--abc=bcd", + "--no-foo", + "--bar", + ], + "v": false, + "verbose": false, + } + `); +}); diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-utils/src/run/flags.ts index 6a2966359ece1..c809a40d8512b 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-utils/src/run/flags.ts @@ -37,7 +37,7 @@ export interface Flags { } export function getFlags(argv: string[], options: Options): Flags { - const unexpected: string[] = []; + const unexpectedNames = new Set(); const flagOpts = options.flags || {}; const { verbose, quiet, silent, debug, help, _, ...others } = getopts(argv, { @@ -49,15 +49,64 @@ export function getFlags(argv: string[], options: Options): Flags { }, default: flagOpts.default, unknown: (name: string) => { - unexpected.push(name); + unexpectedNames.add(name); + return flagOpts.guessTypesForUnexpectedFlags; + }, + } as any); + + const unexpected: string[] = []; + for (const unexpectedName of unexpectedNames) { + const matchingArgv: string[] = []; + + iterArgv: for (const [i, v] of argv.entries()) { + for (const prefix of ['--', '-']) { + if (v.startsWith(prefix)) { + // -/--name=value + if (v.startsWith(`${prefix}${unexpectedName}=`)) { + matchingArgv.push(v); + continue iterArgv; + } + + // -/--name (value possibly follows) + if (v === `${prefix}${unexpectedName}`) { + matchingArgv.push(v); - if (options.flags && options.flags.allowUnexpected) { - return true; + // value follows -/--name + if (argv.length > i + 1 && !argv[i + 1].startsWith('-')) { + matchingArgv.push(argv[i + 1]); + } + + continue iterArgv; + } + } } - return false; - }, - } as any); + // special case for `--no-{flag}` disabling of boolean flags + if (v === `--no-${unexpectedName}`) { + matchingArgv.push(v); + continue iterArgv; + } + + // special case for shortcut flags formatted as `-abc` where `a`, `b`, + // and `c` will be three separate unexpected flags + if ( + unexpectedName.length === 1 && + v[0] === '-' && + v[1] !== '-' && + !v.includes('=') && + v.includes(unexpectedName) + ) { + matchingArgv.push(`-${unexpectedName}`); + continue iterArgv; + } + } + + if (matchingArgv.length) { + unexpected.push(...matchingArgv); + } else { + throw new Error(`unable to find unexpected flag named "${unexpectedName}"`); + } + } return { verbose, @@ -75,7 +124,7 @@ export function getHelp(options: Options) { const usage = options.usage || `node ${relative(process.cwd(), process.argv[1])}`; const optionHelp = ( - dedent((options.flags && options.flags.help) || '') + + dedent(options?.flags?.help || '') + '\n' + dedent` --verbose, -v Log verbosely diff --git a/packages/kbn-dev-utils/src/run/run.ts b/packages/kbn-dev-utils/src/run/run.ts index 06746c663b917..1d28d43575729 100644 --- a/packages/kbn-dev-utils/src/run/run.ts +++ b/packages/kbn-dev-utils/src/run/run.ts @@ -36,6 +36,7 @@ export interface Options { description?: string; flags?: { allowUnexpected?: boolean; + guessTypesForUnexpectedFlags?: boolean; help?: string; alias?: { [key: string]: string | string[] }; boolean?: string[]; @@ -46,7 +47,6 @@ export interface Options { export async function run(fn: RunFn, options: Options = {}) { const flags = getFlags(process.argv.slice(2), options); - const allowUnexpected = options.flags ? options.flags.allowUnexpected : false; if (flags.help) { process.stderr.write(getHelp(options)); @@ -97,7 +97,7 @@ export async function run(fn: RunFn, options: Options = {}) { const cleanupTasks: CleanupTask[] = [unhookExit]; try { - if (!allowUnexpected && flags.unexpected.length) { + if (!options.flags?.allowUnexpected && flags.unexpected.length) { throw createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); } diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 4c519a609d86f..0ec058eeb8a28 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "target", "target": "ES2019", - "declaration": true + "declaration": true, + "declarationMap": true }, "include": [ "src/**/*" diff --git a/packages/kbn-es/src/artifact.js b/packages/kbn-es/src/artifact.js index 19d95e82fe480..9ea78386269d9 100644 --- a/packages/kbn-es/src/artifact.js +++ b/packages/kbn-es/src/artifact.js @@ -27,16 +27,14 @@ const { createHash } = require('crypto'); const path = require('path'); const asyncPipeline = promisify(pipeline); -const V1_VERSIONS_API = 'https://artifacts-api.elastic.co/v1/versions'; +const DAILY_SNAPSHOTS_BASE_URL = 'https://storage.googleapis.com/kibana-ci-es-snapshots-daily'; +const PERMANENT_SNAPSHOTS_BASE_URL = + 'https://storage.googleapis.com/kibana-ci-es-snapshots-permanent'; const { cache } = require('./utils'); const { resolveCustomSnapshotUrl } = require('./custom_snapshots'); const { createCliError, isCliError } = require('./errors'); -const TEST_ES_SNAPSHOT_VERSION = process.env.TEST_ES_SNAPSHOT_VERSION - ? process.env.TEST_ES_SNAPSHOT_VERSION - : 'latest'; - function getChecksumType(checksumUrl) { if (checksumUrl.endsWith('.sha512')) { return 'sha512'; @@ -45,20 +43,6 @@ function getChecksumType(checksumUrl) { throw new Error(`unable to determine checksum type: ${checksumUrl}`); } -function getPlatform(key) { - if (key.includes('-linux-')) { - return 'linux'; - } - - if (key.includes('-windows-')) { - return 'win32'; - } - - if (key.includes('-darwin-')) { - return 'darwin'; - } -} - function headersToString(headers, indent = '') { return [...headers.entries()].reduce( (acc, [key, value]) => `${acc}\n${indent}${key}: ${value}`, @@ -85,6 +69,75 @@ async function retry(log, fn) { return await doAttempt(1); } +// Setting this flag provides an easy way to run the latest un-promoted snapshot without having to look it up +function shouldUseUnverifiedSnapshot() { + return !!process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED; +} + +async function fetchSnapshotManifest(url, log) { + log.info('Downloading snapshot manifest from %s', chalk.bold(url)); + + const abc = new AbortController(); + const resp = await retry(log, async () => await fetch(url, { signal: abc.signal })); + const json = await resp.text(); + + return { abc, resp, json }; +} + +async function getArtifactSpecForSnapshot(urlVersion, license, log) { + const desiredVersion = urlVersion.replace('-SNAPSHOT', ''); + const desiredLicense = license === 'oss' ? 'oss' : 'default'; + + const customManifestUrl = process.env.ES_SNAPSHOT_MANIFEST; + const primaryManifestUrl = `${DAILY_SNAPSHOTS_BASE_URL}/${desiredVersion}/manifest-latest${ + shouldUseUnverifiedSnapshot() ? '' : '-verified' + }.json`; + const secondaryManifestUrl = `${PERMANENT_SNAPSHOTS_BASE_URL}/${desiredVersion}/manifest.json`; + + let { abc, resp, json } = await fetchSnapshotManifest( + customManifestUrl || primaryManifestUrl, + log + ); + + if (!customManifestUrl && !shouldUseUnverifiedSnapshot() && resp.status === 404) { + log.info('Daily snapshot manifest not found, falling back to permanent manifest'); + ({ abc, resp, json } = await fetchSnapshotManifest(secondaryManifestUrl, log)); + } + + if (resp.status === 404) { + abc.abort(); + throw createCliError(`Snapshots for ${desiredVersion} are not available`); + } + + if (!resp.ok) { + abc.abort(); + throw new Error(`Unable to read snapshot manifest: ${resp.statusText}\n ${json}`); + } + + const manifest = JSON.parse(json); + + const platform = process.platform === 'win32' ? 'windows' : process.platform; + const archive = manifest.archives.find( + archive => + archive.version === desiredVersion && + archive.platform === platform && + archive.license === desiredLicense + ); + + if (!archive) { + throw createCliError( + `Snapshots for ${desiredVersion} are available, but couldn't find an artifact in the manifest for [${desiredVersion}, ${desiredLicense}, ${platform}]` + ); + } + + return { + url: archive.url, + checksumUrl: archive.url + '.sha512', + checksumType: 'sha512', + filename: archive.filename, + }; +} + exports.Artifact = class Artifact { /** * Fetch an Artifact from the Artifact API for a license level and version @@ -100,71 +153,7 @@ exports.Artifact = class Artifact { return new Artifact(customSnapshotArtifactSpec, log); } - const urlBuild = encodeURIComponent(TEST_ES_SNAPSHOT_VERSION); - const url = `${V1_VERSIONS_API}/${urlVersion}/builds/${urlBuild}/projects/elasticsearch`; - - const json = await retry(log, async () => { - log.info('downloading artifact info from %s', chalk.bold(url)); - - const abc = new AbortController(); - const resp = await fetch(url, { signal: abc.signal }); - const json = await resp.text(); - - if (resp.status === 404) { - abc.abort(); - throw createCliError( - `Snapshots for ${version}/${TEST_ES_SNAPSHOT_VERSION} are not available` - ); - } - - if (!resp.ok) { - abc.abort(); - throw new Error(`Unable to read artifact info from ${url}: ${resp.statusText}\n ${json}`); - } - - return json; - }); - - // parse the api response into an array of Artifact objects - const { - project: { packages: artifactInfoMap }, - } = JSON.parse(json); - const filenames = Object.keys(artifactInfoMap); - const hasNoJdkVersions = filenames.some(filename => filename.includes('-no-jdk-')); - const artifactSpecs = filenames.map(filename => ({ - filename, - url: artifactInfoMap[filename].url, - checksumUrl: artifactInfoMap[filename].sha_url, - checksumType: getChecksumType(artifactInfoMap[filename].sha_url), - type: artifactInfoMap[filename].type, - isOss: filename.includes('-oss-'), - platform: getPlatform(filename), - jdkRequired: hasNoJdkVersions ? filename.includes('-no-jdk-') : true, - })); - - // pick the artifact we are going to use for this license/version combo - const reqOss = license === 'oss'; - const reqPlatform = artifactSpecs.some(a => a.platform !== undefined) - ? process.platform - : undefined; - const reqJdkRequired = hasNoJdkVersions ? false : true; - const reqType = process.platform === 'win32' ? 'zip' : 'tar'; - - const artifactSpec = artifactSpecs.find( - spec => - spec.isOss === reqOss && - spec.type === reqType && - spec.platform === reqPlatform && - spec.jdkRequired === reqJdkRequired - ); - - if (!artifactSpec) { - throw new Error( - `Unable to determine artifact for license [${license}] and version [${version}]\n` + - ` options: ${filenames.join(',')}` - ); - } - + const artifactSpec = await getArtifactSpecForSnapshot(urlVersion, license, log); return new Artifact(artifactSpec, log); } diff --git a/packages/kbn-es/src/artifact.test.js b/packages/kbn-es/src/artifact.test.js new file mode 100644 index 0000000000000..985b65c747563 --- /dev/null +++ b/packages/kbn-es/src/artifact.test.js @@ -0,0 +1,191 @@ +/* + * 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 { ToolingLog } from '@kbn/dev-utils'; +jest.mock('node-fetch'); +import fetch from 'node-fetch'; +const { Response } = jest.requireActual('node-fetch'); + +import { Artifact } from './artifact'; + +const log = new ToolingLog(); +let MOCKS; + +const PLATFORM = process.platform === 'win32' ? 'windows' : process.platform; +const MOCK_VERSION = 'test-version'; +const MOCK_URL = 'http://127.0.0.1:12345'; +const MOCK_FILENAME = 'test-filename'; + +const DAILY_SNAPSHOT_BASE_URL = 'https://storage.googleapis.com/kibana-ci-es-snapshots-daily'; +const PERMANENT_SNAPSHOT_BASE_URL = + 'https://storage.googleapis.com/kibana-ci-es-snapshots-permanent'; + +const createArchive = (params = {}) => { + const license = params.license || 'default'; + + return { + license: 'default', + version: MOCK_VERSION, + url: MOCK_URL + `/${license}`, + platform: PLATFORM, + filename: MOCK_FILENAME + `.${license}`, + ...params, + }; +}; + +const mockFetch = mock => + fetch.mockReturnValue(Promise.resolve(new Response(JSON.stringify(mock)))); + +let previousSnapshotManifestValue = null; + +beforeAll(() => { + if ('ES_SNAPSHOT_MANIFEST' in process.env) { + previousSnapshotManifestValue = process.env.ES_SNAPSHOT_MANIFEST; + delete process.env.ES_SNAPSHOT_MANIFEST; + } +}); + +afterAll(() => { + if (previousSnapshotManifestValue !== null) { + process.env.ES_SNAPSHOT_MANIFEST = previousSnapshotManifestValue; + } else { + delete process.env.ES_SNAPSHOT_MANIFEST; + } +}); + +beforeEach(() => { + jest.resetAllMocks(); + + MOCKS = { + valid: { + archives: [createArchive({ license: 'oss' }), createArchive({ license: 'default' })], + }, + }; +}); + +const artifactTest = (requestedLicense, expectedLicense, fetchTimesCalled = 1) => { + return async () => { + const artifact = await Artifact.getSnapshot(requestedLicense, MOCK_VERSION, log); + expect(fetch).toHaveBeenCalledTimes(fetchTimesCalled); + expect(fetch.mock.calls[0][0]).toEqual( + `${DAILY_SNAPSHOT_BASE_URL}/${MOCK_VERSION}/manifest-latest-verified.json` + ); + if (fetchTimesCalled === 2) { + expect(fetch.mock.calls[1][0]).toEqual( + `${PERMANENT_SNAPSHOT_BASE_URL}/${MOCK_VERSION}/manifest.json` + ); + } + expect(artifact.getUrl()).toEqual(MOCK_URL + `/${expectedLicense}`); + expect(artifact.getChecksumUrl()).toEqual(MOCK_URL + `/${expectedLicense}.sha512`); + expect(artifact.getChecksumType()).toEqual('sha512'); + expect(artifact.getFilename()).toEqual(MOCK_FILENAME + `.${expectedLicense}`); + }; +}; + +describe('Artifact', () => { + describe('getSnapshot()', () => { + describe('with default snapshot', () => { + beforeEach(() => { + mockFetch(MOCKS.valid); + }); + + it('should return artifact metadata for a daily oss artifact', artifactTest('oss', 'oss')); + + it( + 'should return artifact metadata for a daily default artifact', + artifactTest('default', 'default') + ); + + it( + 'should default to default license with anything other than "oss"', + artifactTest('INVALID_LICENSE', 'default') + ); + + it('should throw when an artifact cannot be found in the manifest for the specified parameters', async () => { + await expect(Artifact.getSnapshot('default', 'INVALID_VERSION', log)).rejects.toThrow( + "couldn't find an artifact" + ); + }); + }); + + describe('with missing default snapshot', () => { + beforeEach(() => { + fetch.mockReturnValueOnce(Promise.resolve(new Response('', { status: 404 }))); + mockFetch(MOCKS.valid); + }); + + it( + 'should return artifact metadata for a permanent oss artifact', + artifactTest('oss', 'oss', 2) + ); + + it( + 'should return artifact metadata for a permanent default artifact', + artifactTest('default', 'default', 2) + ); + + it( + 'should default to default license with anything other than "oss"', + artifactTest('INVALID_LICENSE', 'default', 2) + ); + + it('should throw when an artifact cannot be found in the manifest for the specified parameters', async () => { + await expect(Artifact.getSnapshot('default', 'INVALID_VERSION', log)).rejects.toThrow( + "couldn't find an artifact" + ); + }); + }); + + describe('with custom snapshot manifest URL', () => { + const CUSTOM_URL = 'http://www.creedthoughts.gov.www/creedthoughts'; + + beforeEach(() => { + process.env.ES_SNAPSHOT_MANIFEST = CUSTOM_URL; + mockFetch(MOCKS.valid); + }); + + it('should use the custom URL when looking for a snapshot', async () => { + await Artifact.getSnapshot('oss', MOCK_VERSION, log); + expect(fetch.mock.calls[0][0]).toEqual(CUSTOM_URL); + }); + + afterEach(() => { + delete process.env.ES_SNAPSHOT_MANIFEST; + }); + }); + + describe('with latest unverified snapshot', () => { + beforeEach(() => { + process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 1; + mockFetch(MOCKS.valid); + }); + + it('should use the daily unverified URL when looking for a snapshot', async () => { + await Artifact.getSnapshot('oss', MOCK_VERSION, log); + expect(fetch.mock.calls[0][0]).toEqual( + `${DAILY_SNAPSHOT_BASE_URL}/${MOCK_VERSION}/manifest-latest.json` + ); + }); + + afterEach(() => { + delete process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED; + }); + }); + }); +}); diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 665f80e3802e3..ceb4a5b6aece1 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -35,7 +35,7 @@ const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); const { parseSettings, SettingsFilter } = require('./settings'); -const { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); +const { CA_CERT_PATH, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils'); const readFile = util.promisify(fs.readFile); // listen to data on stream until map returns anything but undefined @@ -261,9 +261,9 @@ exports.Cluster = class Cluster { const esArgs = [].concat(options.esArgs || []); if (this._ssl) { esArgs.push('xpack.security.http.ssl.enabled=true'); - esArgs.push(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); - esArgs.push(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); - esArgs.push(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + esArgs.push(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); + esArgs.push(`xpack.security.http.ssl.keystore.type=PKCS12`); + esArgs.push(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`); } const args = parseSettings(extractConfigFiles(esArgs, installPath, { log: this._log }), { diff --git a/packages/kbn-es/src/custom_snapshots.js b/packages/kbn-es/src/custom_snapshots.js index 74de3c2c792fd..c6b00f77f0a88 100644 --- a/packages/kbn-es/src/custom_snapshots.js +++ b/packages/kbn-es/src/custom_snapshots.js @@ -25,8 +25,13 @@ function isVersionFlag(a) { function getCustomSnapshotUrl() { // force use of manually created snapshots until ReindexPutMappings fix - if (!process.env.KBN_ES_SNAPSHOT_URL && !process.argv.some(isVersionFlag)) { - return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + if ( + !process.env.ES_SNAPSHOT_MANIFEST && + !process.env.KBN_ES_SNAPSHOT_URL && + !process.argv.some(isVersionFlag) + ) { + // return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + return; } if (process.env.KBN_ES_SNAPSHOT_URL && process.env.KBN_ES_SNAPSHOT_URL !== 'false') { diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js index d3181a748ffbb..d374abe5db068 100644 --- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -34,6 +34,8 @@ if (!start) { let serverUrl; const server = createServer( { + // Note: the integration uses the ES_P12_PATH, but that keystore contains + // the same key/cert as ES_KEY_PATH and ES_CERT_PATH key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, }, diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index dd570e27e3282..dfbc04477bd40 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -17,7 +17,7 @@ * under the License. */ -const { ToolingLog, CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); +const { ToolingLog, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils'); const execa = require('execa'); const { Cluster } = require('../cluster'); const { installSource, installSnapshot, installArchive } = require('../install'); @@ -252,9 +252,9 @@ describe('#start(installPath)', () => { const config = extractConfigFiles.mock.calls[0][0]; expect(config).toContain('xpack.security.http.ssl.enabled=true'); - expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.type=PKCS12`); + expect(config).toContain(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`); }); it(`doesn't setup SSL when disabled`, async () => { @@ -319,9 +319,9 @@ describe('#run()', () => { const config = extractConfigFiles.mock.calls[0][0]; expect(config).toContain('xpack.security.http.ssl.enabled=true'); - expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.type=PKCS12`); + expect(config).toContain(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`); }); it(`doesn't setup SSL when disabled`, async () => { diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index c51168ae2d91c..e02c38494991a 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -30,8 +30,6 @@ exports.getWebpackConfig = function(kibanaPath, projectRoot, config) { ui: fromKibana('src/legacy/ui/public'), test_harness: fromKibana('src/test_harness/public'), querystring: 'querystring-browser', - moment$: fromKibana('webpackShims/moment'), - 'moment-timezone$': fromKibana('webpackShims/moment-timezone'), // Dev defaults for test bundle https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/core_plugins/tests_bundle/index.js#L73-L78 ng_mock$: fromKibana('src/test_utils/public/ng_mock'), diff --git a/packages/kbn-eslint-plugin-eslint/package.json b/packages/kbn-eslint-plugin-eslint/package.json index badcf13187caf..026938213ac83 100644 --- a/packages/kbn-eslint-plugin-eslint/package.json +++ b/packages/kbn-eslint-plugin-eslint/package.json @@ -4,12 +4,12 @@ "private": true, "license": "Apache-2.0", "peerDependencies": { - "eslint": "6.5.1", + "eslint": "6.8.0", "babel-eslint": "^10.0.3" }, "dependencies": { "micromatch": "3.1.10", "dedent": "^0.7.0", - "eslint-module-utils": "2.4.1" + "eslint-module-utils": "2.5.0" } } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 7c5937af441a2..a3debf78fb8c8 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -4500,6 +4500,14 @@ var certs_1 = __webpack_require__(422); exports.CA_CERT_PATH = certs_1.CA_CERT_PATH; exports.ES_KEY_PATH = certs_1.ES_KEY_PATH; exports.ES_CERT_PATH = certs_1.ES_CERT_PATH; +exports.ES_P12_PATH = certs_1.ES_P12_PATH; +exports.ES_P12_PASSWORD = certs_1.ES_P12_PASSWORD; +exports.ES_EMPTYPASSWORD_P12_PATH = certs_1.ES_EMPTYPASSWORD_P12_PATH; +exports.ES_NOPASSWORD_P12_PATH = certs_1.ES_NOPASSWORD_P12_PATH; +exports.KBN_KEY_PATH = certs_1.KBN_KEY_PATH; +exports.KBN_CERT_PATH = certs_1.KBN_CERT_PATH; +exports.KBN_P12_PATH = certs_1.KBN_P12_PATH; +exports.KBN_P12_PASSWORD = certs_1.KBN_P12_PASSWORD; var run_1 = __webpack_require__(423); exports.run = run_1.run; exports.createFailError = run_1.createFailError; @@ -36986,6 +36994,14 @@ const path_1 = __webpack_require__(16); exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt'); exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key'); exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt'); +exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12'); +exports.ES_P12_PASSWORD = 'storepass'; +exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12'); +exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); +exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key'); +exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt'); +exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12'); +exports.KBN_P12_PASSWORD = 'storepass'; /***/ }), @@ -37054,8 +37070,8 @@ const tooling_log_1 = __webpack_require__(415); const fail_1 = __webpack_require__(425); const flags_1 = __webpack_require__(426); async function run(fn, options = {}) { + var _a; const flags = flags_1.getFlags(process.argv.slice(2), options); - const allowUnexpected = options.flags ? options.flags.allowUnexpected : false; if (flags.help) { process.stderr.write(flags_1.getHelp(options)); process.exit(1); @@ -37098,7 +37114,7 @@ async function run(fn, options = {}) { const unhookExit = exit_hook_1.default(doCleanup); const cleanupTasks = [unhookExit]; try { - if (!allowUnexpected && flags.unexpected.length) { + if (!((_a = options.flags) === null || _a === void 0 ? void 0 : _a.allowUnexpected) && flags.unexpected.length) { throw fail_1.createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); } try { @@ -37218,7 +37234,7 @@ const path_1 = __webpack_require__(16); const dedent_1 = tslib_1.__importDefault(__webpack_require__(14)); const getopts_1 = tslib_1.__importDefault(__webpack_require__(427)); function getFlags(argv, options) { - const unexpected = []; + const unexpectedNames = new Set(); const flagOpts = options.flags || {}; const { verbose, quiet, silent, debug, help, _, ...others } = getopts_1.default(argv, { string: flagOpts.string, @@ -37229,13 +37245,55 @@ function getFlags(argv, options) { }, default: flagOpts.default, unknown: (name) => { - unexpected.push(name); - if (options.flags && options.flags.allowUnexpected) { - return true; - } - return false; + unexpectedNames.add(name); + return flagOpts.guessTypesForUnexpectedFlags; }, }); + const unexpected = []; + for (const unexpectedName of unexpectedNames) { + const matchingArgv = []; + iterArgv: for (const [i, v] of argv.entries()) { + for (const prefix of ['--', '-']) { + if (v.startsWith(prefix)) { + // -/--name=value + if (v.startsWith(`${prefix}${unexpectedName}=`)) { + matchingArgv.push(v); + continue iterArgv; + } + // -/--name (value possibly follows) + if (v === `${prefix}${unexpectedName}`) { + matchingArgv.push(v); + // value follows -/--name + if (argv.length > i + 1 && !argv[i + 1].startsWith('-')) { + matchingArgv.push(argv[i + 1]); + } + continue iterArgv; + } + } + } + // special case for `--no-{flag}` disabling of boolean flags + if (v === `--no-${unexpectedName}`) { + matchingArgv.push(v); + continue iterArgv; + } + // special case for shortcut flags formatted as `-abc` where `a`, `b`, + // and `c` will be three separate unexpected flags + if (unexpectedName.length === 1 && + v[0] === '-' && + v[1] !== '-' && + !v.includes('=') && + v.includes(unexpectedName)) { + matchingArgv.push(`-${unexpectedName}`); + continue iterArgv; + } + } + if (matchingArgv.length) { + unexpected.push(...matchingArgv); + } + else { + throw new Error(`unable to find unexpected flag named "${unexpectedName}"`); + } + } return { verbose, quiet, @@ -37249,8 +37307,9 @@ function getFlags(argv, options) { } exports.getFlags = getFlags; function getHelp(options) { + var _a, _b; const usage = options.usage || `node ${path_1.relative(process.cwd(), process.argv[1])}`; - const optionHelp = (dedent_1.default((options.flags && options.flags.help) || '') + + const optionHelp = (dedent_1.default(((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.flags) === null || _b === void 0 ? void 0 : _b.help) || '') + '\n' + dedent_1.default ` --verbose, -v Log verbosely @@ -58209,6 +58268,7 @@ function getProjectPaths({ if (!ossOnly) { projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/plugins/*')); projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/legacy/plugins/*')); } @@ -79815,7 +79875,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(704); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(909); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(914); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79997,8 +80057,8 @@ const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const arrify = __webpack_require__(706); const globby = __webpack_require__(707); -const cpFile = __webpack_require__(900); -const CpyError = __webpack_require__(907); +const cpFile = __webpack_require__(905); +const CpyError = __webpack_require__(912); const preprocessSrcPath = (srcPath, options) => options.cwd ? path.resolve(options.cwd, srcPath) : srcPath; @@ -80127,8 +80187,8 @@ const fs = __webpack_require__(23); const arrayUnion = __webpack_require__(708); const glob = __webpack_require__(502); const fastGlob = __webpack_require__(710); -const dirGlob = __webpack_require__(893); -const gitignore = __webpack_require__(896); +const dirGlob = __webpack_require__(898); +const gitignore = __webpack_require__(901); const DEFAULT_FILTER = () => false; @@ -80379,11 +80439,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(712); var taskManager = __webpack_require__(713); -var reader_async_1 = __webpack_require__(864); -var reader_stream_1 = __webpack_require__(888); -var reader_sync_1 = __webpack_require__(889); -var arrayUtils = __webpack_require__(891); -var streamUtils = __webpack_require__(892); +var reader_async_1 = __webpack_require__(869); +var reader_stream_1 = __webpack_require__(893); +var reader_sync_1 = __webpack_require__(894); +var arrayUtils = __webpack_require__(896); +var streamUtils = __webpack_require__(897); /** * Synchronous API. */ @@ -81023,9 +81083,9 @@ var extend = __webpack_require__(830); */ var compilers = __webpack_require__(833); -var parsers = __webpack_require__(860); -var cache = __webpack_require__(861); -var utils = __webpack_require__(862); +var parsers = __webpack_require__(865); +var cache = __webpack_require__(866); +var utils = __webpack_require__(867); var MAX_LENGTH = 1024 * 64; /** @@ -99558,9 +99618,9 @@ var toRegex = __webpack_require__(721); */ var compilers = __webpack_require__(850); -var parsers = __webpack_require__(856); -var Extglob = __webpack_require__(859); -var utils = __webpack_require__(858); +var parsers = __webpack_require__(861); +var Extglob = __webpack_require__(864); +var utils = __webpack_require__(863); var MAX_LENGTH = 1024 * 64; /** @@ -100070,7 +100130,7 @@ var parsers = __webpack_require__(854); * Module dependencies */ -var debug = __webpack_require__(793)('expand-brackets'); +var debug = __webpack_require__(856)('expand-brackets'); var extend = __webpack_require__(730); var Snapdragon = __webpack_require__(760); var toRegex = __webpack_require__(721); @@ -100664,12 +100724,839 @@ exports.createRegex = function(pattern, include) { /* 856 */ /***/ (function(module, exports, __webpack_require__) { +/** + * Detect Electron renderer process, which is node, but we should + * treat as a browser. + */ + +if (typeof process !== 'undefined' && process.type === 'renderer') { + module.exports = __webpack_require__(857); +} else { + module.exports = __webpack_require__(860); +} + + +/***/ }), +/* 857 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * This is the web browser implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = __webpack_require__(858); +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; +exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); + +/** + * Colors. + */ + +exports.colors = [ + 'lightseagreen', + 'forestgreen', + 'goldenrod', + 'dodgerblue', + 'darkorchid', + 'crimson' +]; + +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } + + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs(args) { + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return; + + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} + + +/***/ }), +/* 858 */ +/***/ (function(module, exports, __webpack_require__) { + + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = __webpack_require__(859); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + +exports.formatters = {}; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + + +/***/ }), +/* 859 */ +/***/ (function(module, exports) { + +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} + + +/***/ }), +/* 860 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * Module dependencies. + */ + +var tty = __webpack_require__(480); +var util = __webpack_require__(29); + +/** + * This is the Node.js implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = __webpack_require__(858); +exports.init = init; +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; + +/** + * Colors. + */ + +exports.colors = [6, 2, 3, 4, 5, 1]; + +/** + * Build up the default `inspectOpts` object from the environment variables. + * + * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js + */ + +exports.inspectOpts = Object.keys(process.env).filter(function (key) { + return /^debug_/i.test(key); +}).reduce(function (obj, key) { + // camel-case + var prop = key + .substring(6) + .toLowerCase() + .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); + + // coerce string value into JS value + var val = process.env[key]; + if (/^(yes|on|true|enabled)$/i.test(val)) val = true; + else if (/^(no|off|false|disabled)$/i.test(val)) val = false; + else if (val === 'null') val = null; + else val = Number(val); + + obj[prop] = val; + return obj; +}, {}); + +/** + * The file descriptor to write the `debug()` calls to. + * Set the `DEBUG_FD` env variable to override with another value. i.e.: + * + * $ DEBUG_FD=3 node script.js 3>debug.log + */ + +var fd = parseInt(process.env.DEBUG_FD, 10) || 2; + +if (1 !== fd && 2 !== fd) { + util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')() +} + +var stream = 1 === fd ? process.stdout : + 2 === fd ? process.stderr : + createWritableStdioStream(fd); + +/** + * Is stdout a TTY? Colored output is enabled when `true`. + */ + +function useColors() { + return 'colors' in exports.inspectOpts + ? Boolean(exports.inspectOpts.colors) + : tty.isatty(fd); +} + +/** + * Map %o to `util.inspect()`, all on a single line. + */ + +exports.formatters.o = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts) + .split('\n').map(function(str) { + return str.trim() + }).join(' '); +}; + +/** + * Map %o to `util.inspect()`, allowing multiple lines if needed. + */ + +exports.formatters.O = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts); +}; + +/** + * Adds ANSI color escape codes if enabled. + * + * @api public + */ + +function formatArgs(args) { + var name = this.namespace; + var useColors = this.useColors; + + if (useColors) { + var c = this.color; + var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m'; + + args[0] = prefix + args[0].split('\n').join('\n' + prefix); + args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); + } else { + args[0] = new Date().toUTCString() + + ' ' + name + ' ' + args[0]; + } +} + +/** + * Invokes `util.format()` with the specified arguments and writes to `stream`. + */ + +function log() { + return stream.write(util.format.apply(util, arguments) + '\n'); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + if (null == namespaces) { + // If you set a process.env field to null or undefined, it gets cast to the + // string 'null' or 'undefined'. Just delete instead. + delete process.env.DEBUG; + } else { + process.env.DEBUG = namespaces; + } +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + return process.env.DEBUG; +} + +/** + * Copied from `node/src/node.js`. + * + * XXX: It's lame that node doesn't expose this API out-of-the-box. It also + * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. + */ + +function createWritableStdioStream (fd) { + var stream; + var tty_wrap = process.binding('tty_wrap'); + + // Note stream._type is used for test-module-load-list.js + + switch (tty_wrap.guessHandleType(fd)) { + case 'TTY': + stream = new tty.WriteStream(fd); + stream._type = 'tty'; + + // Hack to have stream not keep the event loop alive. + // See https://github.com/joyent/node/issues/1726 + if (stream._handle && stream._handle.unref) { + stream._handle.unref(); + } + break; + + case 'FILE': + var fs = __webpack_require__(23); + stream = new fs.SyncWriteStream(fd, { autoClose: false }); + stream._type = 'fs'; + break; + + case 'PIPE': + case 'TCP': + var net = __webpack_require__(798); + stream = new net.Socket({ + fd: fd, + readable: false, + writable: true + }); + + // FIXME Should probably have an option in net.Socket to create a + // stream from an existing fd which is writable only. But for now + // we'll just add this hack and set the `readable` member to false. + // Test: ./node test/fixtures/echo.js < /etc/passwd + stream.readable = false; + stream.read = null; + stream._type = 'pipe'; + + // FIXME Hack to have stream not keep the event loop alive. + // See https://github.com/joyent/node/issues/1726 + if (stream._handle && stream._handle.unref) { + stream._handle.unref(); + } + break; + + default: + // Probably an error on in uv_guess_handle() + throw new Error('Implement me. Unknown stream file type!'); + } + + // For supporting legacy API we put the FD here. + stream.fd = fd; + + stream._isStdio = true; + + return stream; +} + +/** + * Init logic for `debug` instances. + * + * Create a new `inspectOpts` object in case `useColors` is set + * differently for a particular `debug` instance. + */ + +function init (debug) { + debug.inspectOpts = {}; + + var keys = Object.keys(exports.inspectOpts); + for (var i = 0; i < keys.length; i++) { + debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; + } +} + +/** + * Enable namespaces listed in `process.env.DEBUG` initially. + */ + +exports.enable(load()); + + +/***/ }), +/* 861 */ +/***/ (function(module, exports, __webpack_require__) { + "use strict"; var brackets = __webpack_require__(851); -var define = __webpack_require__(857); -var utils = __webpack_require__(858); +var define = __webpack_require__(862); +var utils = __webpack_require__(863); /** * Characters to use in text regex (we want to "not" match @@ -100824,7 +101711,7 @@ module.exports = parsers; /***/ }), -/* 857 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100862,7 +101749,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 858 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100938,7 +101825,7 @@ utils.createRegex = function(str) { /***/ }), -/* 859 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100949,7 +101836,7 @@ utils.createRegex = function(str) { */ var Snapdragon = __webpack_require__(760); -var define = __webpack_require__(857); +var define = __webpack_require__(862); var extend = __webpack_require__(730); /** @@ -100957,7 +101844,7 @@ var extend = __webpack_require__(730); */ var compilers = __webpack_require__(850); -var parsers = __webpack_require__(856); +var parsers = __webpack_require__(861); /** * Customize Snapdragon parser and renderer @@ -101023,7 +101910,7 @@ module.exports = Extglob; /***/ }), -/* 860 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101113,14 +102000,14 @@ function textRegex(pattern) { /***/ }), -/* 861 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { module.exports = new (__webpack_require__(842))(); /***/ }), -/* 862 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101138,7 +102025,7 @@ utils.define = __webpack_require__(829); utils.diff = __webpack_require__(846); utils.extend = __webpack_require__(830); utils.pick = __webpack_require__(847); -utils.typeOf = __webpack_require__(863); +utils.typeOf = __webpack_require__(868); utils.unique = __webpack_require__(733); /** @@ -101436,7 +102323,7 @@ utils.unixify = function(options) { /***/ }), -/* 863 */ +/* 868 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -101571,7 +102458,7 @@ function isBuffer(val) { /***/ }), -/* 864 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101590,9 +102477,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(865); -var reader_1 = __webpack_require__(878); -var fs_stream_1 = __webpack_require__(882); +var readdir = __webpack_require__(870); +var reader_1 = __webpack_require__(883); +var fs_stream_1 = __webpack_require__(887); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -101653,15 +102540,15 @@ exports.default = ReaderAsync; /***/ }), -/* 865 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(866); -const readdirAsync = __webpack_require__(874); -const readdirStream = __webpack_require__(877); +const readdirSync = __webpack_require__(871); +const readdirAsync = __webpack_require__(879); +const readdirStream = __webpack_require__(882); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -101745,7 +102632,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 866 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101753,11 +102640,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(867); +const DirectoryReader = __webpack_require__(872); let syncFacade = { - fs: __webpack_require__(872), - forEach: __webpack_require__(873), + fs: __webpack_require__(877), + forEach: __webpack_require__(878), sync: true }; @@ -101786,7 +102673,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 867 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101795,9 +102682,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(868); -const stat = __webpack_require__(870); -const call = __webpack_require__(871); +const normalizeOptions = __webpack_require__(873); +const stat = __webpack_require__(875); +const call = __webpack_require__(876); /** * Asynchronously reads the contents of a directory and streams the results @@ -102173,14 +103060,14 @@ module.exports = DirectoryReader; /***/ }), -/* 868 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(869); +const globToRegExp = __webpack_require__(874); module.exports = normalizeOptions; @@ -102357,7 +103244,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 869 */ +/* 874 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -102494,13 +103381,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 870 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(871); +const call = __webpack_require__(876); module.exports = stat; @@ -102575,7 +103462,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 871 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102636,14 +103523,14 @@ function callOnce (fn) { /***/ }), -/* 872 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(871); +const call = __webpack_require__(876); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -102707,7 +103594,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 873 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102736,7 +103623,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 874 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102744,12 +103631,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(875); -const DirectoryReader = __webpack_require__(867); +const maybe = __webpack_require__(880); +const DirectoryReader = __webpack_require__(872); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(876), + forEach: __webpack_require__(881), async: true }; @@ -102791,7 +103678,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 875 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102818,7 +103705,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 876 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102854,7 +103741,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 877 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102862,11 +103749,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(867); +const DirectoryReader = __webpack_require__(872); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(876), + forEach: __webpack_require__(881), async: true }; @@ -102886,16 +103773,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 878 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(879); -var entry_1 = __webpack_require__(881); -var pathUtil = __webpack_require__(880); +var deep_1 = __webpack_require__(884); +var entry_1 = __webpack_require__(886); +var pathUtil = __webpack_require__(885); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -102961,13 +103848,13 @@ exports.default = Reader; /***/ }), -/* 879 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(880); +var pathUtils = __webpack_require__(885); var patternUtils = __webpack_require__(714); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -103051,7 +103938,7 @@ exports.default = DeepFilter; /***/ }), -/* 880 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103082,13 +103969,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 881 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(880); +var pathUtils = __webpack_require__(885); var patternUtils = __webpack_require__(714); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -103174,7 +104061,7 @@ exports.default = EntryFilter; /***/ }), -/* 882 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103194,8 +104081,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(883); -var fs_1 = __webpack_require__(887); +var fsStat = __webpack_require__(888); +var fs_1 = __webpack_require__(892); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -103245,14 +104132,14 @@ exports.default = FileSystemStream; /***/ }), -/* 883 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(884); -const statProvider = __webpack_require__(886); +const optionsManager = __webpack_require__(889); +const statProvider = __webpack_require__(891); /** * Asynchronous API. */ @@ -103283,13 +104170,13 @@ exports.statSync = statSync; /***/ }), -/* 884 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(885); +const fsAdapter = __webpack_require__(890); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -103302,7 +104189,7 @@ exports.prepare = prepare; /***/ }), -/* 885 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103325,7 +104212,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 886 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103377,7 +104264,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 887 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103408,7 +104295,7 @@ exports.default = FileSystem; /***/ }), -/* 888 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103428,9 +104315,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(865); -var reader_1 = __webpack_require__(878); -var fs_stream_1 = __webpack_require__(882); +var readdir = __webpack_require__(870); +var reader_1 = __webpack_require__(883); +var fs_stream_1 = __webpack_require__(887); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -103498,7 +104385,7 @@ exports.default = ReaderStream; /***/ }), -/* 889 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103517,9 +104404,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(865); -var reader_1 = __webpack_require__(878); -var fs_sync_1 = __webpack_require__(890); +var readdir = __webpack_require__(870); +var reader_1 = __webpack_require__(883); +var fs_sync_1 = __webpack_require__(895); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -103579,7 +104466,7 @@ exports.default = ReaderSync; /***/ }), -/* 890 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103598,8 +104485,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(883); -var fs_1 = __webpack_require__(887); +var fsStat = __webpack_require__(888); +var fs_1 = __webpack_require__(892); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -103645,7 +104532,7 @@ exports.default = FileSystemSync; /***/ }), -/* 891 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103661,7 +104548,7 @@ exports.flatten = flatten; /***/ }), -/* 892 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103682,13 +104569,13 @@ exports.merge = merge; /***/ }), -/* 893 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(894); +const pathType = __webpack_require__(899); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -103754,13 +104641,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 894 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(895); +const pify = __webpack_require__(900); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -103803,7 +104690,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 895 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103894,7 +104781,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 896 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103902,9 +104789,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const fastGlob = __webpack_require__(710); -const gitIgnore = __webpack_require__(897); -const pify = __webpack_require__(898); -const slash = __webpack_require__(899); +const gitIgnore = __webpack_require__(902); +const pify = __webpack_require__(903); +const slash = __webpack_require__(904); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -104002,7 +104889,7 @@ module.exports.sync = options => { /***/ }), -/* 897 */ +/* 902 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -104471,7 +105358,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 898 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104546,7 +105433,7 @@ module.exports = (input, options) => { /***/ }), -/* 899 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104564,17 +105451,17 @@ module.exports = input => { /***/ }), -/* 900 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const {Buffer} = __webpack_require__(901); -const CpFileError = __webpack_require__(902); -const fs = __webpack_require__(904); -const ProgressEmitter = __webpack_require__(906); +const {Buffer} = __webpack_require__(906); +const CpFileError = __webpack_require__(907); +const fs = __webpack_require__(909); +const ProgressEmitter = __webpack_require__(911); const cpFile = (source, destination, options) => { if (!source || !destination) { @@ -104728,7 +105615,7 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 901 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { /* eslint-disable node/no-deprecated-api */ @@ -104796,12 +105683,12 @@ SafeBuffer.allocUnsafeSlow = function (size) { /***/ }), -/* 902 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(903); +const NestedError = __webpack_require__(908); class CpFileError extends NestedError { constructor(message, nested) { @@ -104815,7 +105702,7 @@ module.exports = CpFileError; /***/ }), -/* 903 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(509); @@ -104869,15 +105756,15 @@ module.exports = NestedError; /***/ }), -/* 904 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(22); const makeDir = __webpack_require__(559); -const pify = __webpack_require__(905); -const CpFileError = __webpack_require__(902); +const pify = __webpack_require__(910); +const CpFileError = __webpack_require__(907); const fsP = pify(fs); @@ -105022,7 +105909,7 @@ if (fs.copyFileSync) { /***/ }), -/* 905 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105097,7 +105984,7 @@ module.exports = (input, options) => { /***/ }), -/* 906 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105138,12 +106025,12 @@ module.exports = ProgressEmitter; /***/ }), -/* 907 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(908); +const NestedError = __webpack_require__(913); class CpyError extends NestedError { constructor(message, nested) { @@ -105157,7 +106044,7 @@ module.exports = CpyError; /***/ }), -/* 908 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -105213,7 +106100,7 @@ module.exports = NestedError; /***/ }), -/* 909 */ +/* 914 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/src/config.ts b/packages/kbn-pm/src/config.ts index 6d5e67dca7d13..6ba8d58a26f88 100644 --- a/packages/kbn-pm/src/config.ts +++ b/packages/kbn-pm/src/config.ts @@ -46,6 +46,7 @@ export function getProjectPaths({ rootPath, ossOnly, skipKibanaPlugins }: Option if (!ossOnly) { projectPaths.push(resolve(rootPath, 'x-pack')); + projectPaths.push(resolve(rootPath, 'x-pack/plugins/*')); projectPaths.push(resolve(rootPath, 'x-pack/legacy/plugins/*')); } diff --git a/packages/kbn-ui-shared-deps/README.md b/packages/kbn-ui-shared-deps/README.md new file mode 100644 index 0000000000000..3d3ee37ca5a75 --- /dev/null +++ b/packages/kbn-ui-shared-deps/README.md @@ -0,0 +1,3 @@ +# `@kbn/ui-shared-deps` + +Shared dependencies that must only have a single instance are installed and re-exported from here. To consume them, import the package and merge the `externals` export into your webpack config so that all references to the supported modules will be remapped to use the global versions. \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js new file mode 100644 index 0000000000000..250abd162f91d --- /dev/null +++ b/packages/kbn-ui-shared-deps/entry.js @@ -0,0 +1,39 @@ +/* + * 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. + */ + +// must load before angular +export const Jquery = require('jquery'); +window.$ = window.jQuery = Jquery; + +export const Angular = require('angular'); +export const ElasticCharts = require('@elastic/charts'); +export const ElasticEui = require('@elastic/eui'); +export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); +export const ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json'); +export const ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json'); +export const Moment = require('moment'); +export const MomentTimezone = require('moment-timezone/moment-timezone'); +export const React = require('react'); +export const ReactDom = require('react-dom'); +export const ReactIntl = require('react-intl'); +export const ReactRouter = require('react-router'); // eslint-disable-line +export const ReactRouterDom = require('react-router-dom'); + +// load timezone data into moment-timezone +Moment.tz.load(require('moment-timezone/data/packed/latest.json')); diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/json.tsx b/packages/kbn-ui-shared-deps/index.d.ts similarity index 58% rename from src/legacy/core_plugins/kbn_doc_views/public/views/json.tsx rename to packages/kbn-ui-shared-deps/index.d.ts index 73167b56bddea..132445bbde745 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/json.tsx +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -16,18 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import { addDocView } from 'ui/registry/doc_views'; -import { i18n } from '@kbn/i18n'; -import { JsonCodeBlock } from './json_code_block'; -/* - * Registration of the the doc view: json - * - used to display an ES hit as pretty printed JSON at Discover +/** + * Absolute path to the distributable directory + */ +export const distDir: string; + +/** + * Filename of the main bundle file in the distributable directory + */ +export const distFilename: string; + +/** + * Filename of the dark-theme css file in the distributable directory + */ +export const darkCssDistFilename: string; + +/** + * Filename of the light-theme css file in the distributable directory + */ +export const lightCssDistFilename: string; + +/** + * Externals mapping inteded to be used in a webpack config */ -addDocView({ - title: i18n.translate('kbnDocViews.json.jsonTitle', { - defaultMessage: 'JSON', - }), - order: 20, - component: JsonCodeBlock, -}); +export const externals: { + [key: string]: string; +}; diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js new file mode 100644 index 0000000000000..cef25295b35d7 --- /dev/null +++ b/packages/kbn-ui-shared-deps/index.js @@ -0,0 +1,41 @@ +/* + * 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. + */ + +const Path = require('path'); + +exports.distDir = Path.resolve(__dirname, 'target'); +exports.distFilename = 'kbn-ui-shared-deps.js'; +exports.lightCssDistFilename = 'kbn-ui-shared-deps.light.css'; +exports.darkCssDistFilename = 'kbn-ui-shared-deps.dark.css'; +exports.externals = { + angular: '__kbnSharedDeps__.Angular', + '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', + '@elastic/eui': '__kbnSharedDeps__.ElasticEui', + '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', + '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.ElasticEuiLightTheme', + '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.ElasticEuiDarkTheme', + jquery: '__kbnSharedDeps__.Jquery', + moment: '__kbnSharedDeps__.Moment', + 'moment-timezone': '__kbnSharedDeps__.MomentTimezone', + react: '__kbnSharedDeps__.React', + 'react-dom': '__kbnSharedDeps__.ReactDom', + 'react-intl': '__kbnSharedDeps__.ReactIntl', + 'react-router': '__kbnSharedDeps__.ReactRouter', + 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', +}; diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json new file mode 100644 index 0000000000000..014467d204d96 --- /dev/null +++ b/packages/kbn-ui-shared-deps/package.json @@ -0,0 +1,29 @@ +{ + "name": "@kbn/ui-shared-deps", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "node scripts/build", + "kbn:bootstrap": "node scripts/build --dev", + "kbn:watch": "node scripts/build --watch" + }, + "devDependencies": { + "@elastic/eui": "17.3.1", + "@elastic/charts": "^16.1.0", + "@kbn/dev-utils": "1.0.0", + "@yarnpkg/lockfile": "^1.1.0", + "angular": "^1.7.9", + "css-loader": "^2.1.1", + "del": "^5.1.0", + "jquery": "^3.4.1", + "mini-css-extract-plugin": "0.8.0", + "moment": "^2.24.0", + "moment-timezone": "^0.5.27", + "react-dom": "^16.12.0", + "react-intl": "^2.8.0", + "react": "^16.12.0", + "read-pkg": "^5.2.0", + "webpack": "4.41.0" + } +} \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps/scripts/build.js b/packages/kbn-ui-shared-deps/scripts/build.js new file mode 100644 index 0000000000000..8b7c22dac24ff --- /dev/null +++ b/packages/kbn-ui-shared-deps/scripts/build.js @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const Path = require('path'); + +const { run, createFailError } = require('@kbn/dev-utils'); +const webpack = require('webpack'); +const Stats = require('webpack/lib/Stats'); +const del = require('del'); + +const { getWebpackConfig } = require('../webpack.config'); + +run( + async ({ log, flags }) => { + log.info('cleaning previous build output'); + await del(Path.resolve(__dirname, '../target')); + + const compiler = webpack( + getWebpackConfig({ + dev: flags.dev, + }) + ); + + /** @param {webpack.Stats} stats */ + const onCompilationComplete = stats => { + const took = Math.round((stats.endTime - stats.startTime) / 1000); + + if (!stats.hasErrors() && !stats.hasWarnings()) { + log.success(`webpack completed in about ${took} seconds`); + return; + } + + throw createFailError( + `webpack failure in about ${took} seconds\n${stats.toString({ + colors: true, + ...Stats.presetToOptions('minimal'), + })}` + ); + }; + + if (flags.watch) { + compiler.hooks.done.tap('report on stats', stats => { + try { + onCompilationComplete(stats); + } catch (error) { + log.error(error.message); + } + }); + + compiler.hooks.watchRun.tap('report on start', () => { + process.stdout.cursorTo(0, 0); + process.stdout.clearScreenDown(); + log.info('Running webpack compilation...'); + }); + + compiler.watch({}, error => { + if (error) { + log.error('Fatal webpack error'); + log.error(error); + process.exit(1); + } + }); + + return; + } + + onCompilationComplete( + await new Promise((resolve, reject) => { + compiler.run((error, stats) => { + if (error) { + reject(error); + } else { + resolve(stats); + } + }); + }) + ); + }, + { + description: 'build @kbn/ui-shared-deps', + flags: { + boolean: ['watch', 'dev'], + help: ` + --watch Run in watch mode + --dev Build development friendly version + `, + }, + } +); diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json new file mode 100644 index 0000000000000..c5c3cba147fcf --- /dev/null +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "index.d.ts" + ] +} diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js new file mode 100644 index 0000000000000..87cca2cc897f8 --- /dev/null +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -0,0 +1,90 @@ +/* + * 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. + */ + +const Path = require('path'); + +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const { REPO_ROOT } = require('@kbn/dev-utils'); +const webpack = require('webpack'); + +const SharedDeps = require('./index'); + +const MOMENT_SRC = require.resolve('moment/min/moment-with-locales.js'); + +exports.getWebpackConfig = ({ dev = false } = {}) => ({ + mode: dev ? 'development' : 'production', + entry: { + [SharedDeps.distFilename.replace(/\.js$/, '')]: './entry.js', + [SharedDeps.darkCssDistFilename.replace(/\.css$/, '')]: [ + '@elastic/eui/dist/eui_theme_dark.css', + '@elastic/charts/dist/theme_only_dark.css', + ], + [SharedDeps.lightCssDistFilename.replace(/\.css$/, '')]: [ + '@elastic/eui/dist/eui_theme_light.css', + '@elastic/charts/dist/theme_only_light.css', + ], + }, + context: __dirname, + devtool: dev ? '#cheap-source-map' : false, + output: { + path: SharedDeps.distDir, + filename: '[name].js', + sourceMapFilename: '[file].map', + publicPath: '__REPLACE_WITH_PUBLIC_PATH__', + devtoolModuleFilenameTemplate: info => + `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, + library: '__kbnSharedDeps__', + }, + + module: { + noParse: [MOMENT_SRC], + rules: [ + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + + resolve: { + alias: { + moment: MOMENT_SRC, + }, + }, + + optimization: { + noEmitOnErrors: true, + }, + + performance: { + // NOTE: we are disabling this as those hints + // are more tailored for the final bundles result + // and not for the webpack compilations performance itself + hints: false, + }, + + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': dev ? '"development"' : '"production"', + }), + ], +}); diff --git a/renovate.json5 b/renovate.json5 index ecc9b3b2ceb62..560403046b0a5 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -46,35 +46,6 @@ enabled: false, }, packageRules: [ - { - groupSlug: 'eslint', - groupName: 'eslint related packages', - packagePatterns: [ - '(\\b|_)eslint(\\b|_)', - ], - }, - { - groupSlug: 'babel', - groupName: 'babel related packages', - packagePatterns: [ - '(\\b|_)babel(\\b|_)', - ], - packageNames: [ - 'core-js', - '@types/core-js', - '@babel/preset-react', - '@types/babel__preset-react', - '@babel/preset-typescript', - '@types/babel__preset-typescript', - ], - }, - { - groupSlug: 'jest', - groupName: 'jest related packages', - packagePatterns: [ - '(\\b|_)jest(\\b|_)', - ], - }, { groupSlug: '@elastic/charts', groupName: '@elastic/charts related packages', @@ -88,31 +59,11 @@ masterIssueApproval: false, }, { - groupSlug: 'mocha', - groupName: 'mocha related packages', - packagePatterns: [ - '(\\b|_)mocha(\\b|_)', - ], - }, - { - groupSlug: 'karma', - groupName: 'karma related packages', - packagePatterns: [ - '(\\b|_)karma(\\b|_)', - ], - }, - { - groupSlug: 'gulp', - groupName: 'gulp related packages', - packagePatterns: [ - '(\\b|_)gulp(\\b|_)', - ], - }, - { - groupSlug: 'grunt', - groupName: 'grunt related packages', - packagePatterns: [ - '(\\b|_)grunt(\\b|_)', + groupSlug: '@reach/router', + groupName: '@reach/router related packages', + packageNames: [ + '@reach/router', + '@types/reach__router', ], }, { @@ -122,126 +73,6 @@ '(\\b|_)angular(\\b|_)', ], }, - { - groupSlug: 'd3', - groupName: 'd3 related packages', - packagePatterns: [ - '(\\b|_)d3(\\b|_)', - ], - }, - { - groupSlug: 'react', - groupName: 'react related packages', - packagePatterns: [ - '(\\b|_)react(\\b|_)', - '(\\b|_)redux(\\b|_)', - '(\\b|_)enzyme(\\b|_)', - ], - packageNames: [ - 'ngreact', - '@types/ngreact', - 'recompose', - '@types/recompose', - 'prop-types', - '@types/prop-types', - 'typescript-fsa-reducers', - '@types/typescript-fsa-reducers', - 'reselect', - '@types/reselect', - ], - }, - { - groupSlug: 'moment', - groupName: 'moment related packages', - packagePatterns: [ - '(\\b|_)moment(\\b|_)', - ], - }, - { - groupSlug: 'graphql', - groupName: 'graphql related packages', - packagePatterns: [ - '(\\b|_)graphql(\\b|_)', - '(\\b|_)apollo(\\b|_)', - ], - }, - { - groupSlug: 'webpack', - groupName: 'webpack related packages', - packagePatterns: [ - '(\\b|_)webpack(\\b|_)', - '(\\b|_)loader(\\b|_)', - '(\\b|_)acorn(\\b|_)', - '(\\b|_)terser(\\b|_)', - ], - packageNames: [ - 'mini-css-extract-plugin', - '@types/mini-css-extract-plugin', - 'chokidar', - '@types/chokidar', - ], - }, - { - groupSlug: 'vega', - groupName: 'vega related packages', - packagePatterns: [ - '(\\b|_)vega(\\b|_)', - ], - enabled: false, - }, - { - groupSlug: 'language server', - groupName: 'language server related packages', - packageNames: [ - 'vscode-jsonrpc', - '@types/vscode-jsonrpc', - 'vscode-languageserver', - '@types/vscode-languageserver', - 'vscode-languageserver-types', - '@types/vscode-languageserver-types', - ], - }, - { - groupSlug: 'hapi', - groupName: 'hapi related packages', - packagePatterns: [ - '(\\b|_)hapi(\\b|_)', - ], - packageNames: [ - 'hapi', - '@types/hapi', - 'joi', - '@types/joi', - 'boom', - '@types/boom', - 'hoek', - '@types/hoek', - 'h2o2', - '@types/h2o2', - '@elastic/good', - '@types/elastic__good', - 'good-squeeze', - '@types/good-squeeze', - 'inert', - '@types/inert', - ], - }, - { - groupSlug: 'dragselect', - groupName: 'dragselect related packages', - packageNames: [ - 'dragselect', - '@types/dragselect', - ], - labels: [ - 'release_note:skip', - 'Team:Operations', - 'renovate', - 'v8.0.0', - 'v7.6.0', - ':ml', - ], - }, { groupSlug: 'api-documenter', groupName: 'api-documenter related packages', @@ -254,47 +85,34 @@ enabled: false, }, { - groupSlug: 'jsts', - groupName: 'jsts related packages', + groupSlug: 'archiver', + groupName: 'archiver related packages', packageNames: [ - 'jsts', - '@types/jsts', - ], - allowedVersions: '^1.6.2', - }, - { - groupSlug: 'storybook', - groupName: 'storybook related packages', - packagePatterns: [ - '(\\b|_)storybook(\\b|_)', + 'archiver', + '@types/archiver', ], }, { - groupSlug: 'typescript', - groupName: 'typescript related packages', + groupSlug: 'babel', + groupName: 'babel related packages', packagePatterns: [ - '(\\b|_)ts(\\b|_)', - '(\\b|_)typescript(\\b|_)', - ], - packageNames: [ - 'tslib', - '@types/tslib', + '(\\b|_)babel(\\b|_)', ], - }, - { - groupSlug: 'json-stable-stringify', - groupName: 'json-stable-stringify related packages', packageNames: [ - 'json-stable-stringify', - '@types/json-stable-stringify', + 'core-js', + '@types/core-js', + '@babel/preset-react', + '@types/babel__preset-react', + '@babel/preset-typescript', + '@types/babel__preset-typescript', ], }, { - groupSlug: 'lodash.clonedeep', - groupName: 'lodash.clonedeep related packages', + groupSlug: 'base64-js', + groupName: 'base64-js related packages', packageNames: [ - 'lodash.clonedeep', - '@types/lodash.clonedeep', + 'base64-js', + '@types/base64-js', ], }, { @@ -321,6 +139,14 @@ '@types/cheerio', ], }, + { + groupSlug: 'chroma-js', + groupName: 'chroma-js related packages', + packageNames: [ + 'chroma-js', + '@types/chroma-js', + ], + }, { groupSlug: 'chromedriver', groupName: 'chromedriver related packages', @@ -337,6 +163,45 @@ '@types/classnames', ], }, + { + groupSlug: 'cmd-shim', + groupName: 'cmd-shim related packages', + packageNames: [ + 'cmd-shim', + '@types/cmd-shim', + ], + }, + { + groupSlug: 'color', + groupName: 'color related packages', + packageNames: [ + 'color', + '@types/color', + ], + }, + { + groupSlug: 'cpy', + groupName: 'cpy related packages', + packageNames: [ + 'cpy', + '@types/cpy', + ], + }, + { + groupSlug: 'cytoscape', + groupName: 'cytoscape related packages', + packageNames: [ + 'cytoscape', + '@types/cytoscape', + ], + }, + { + groupSlug: 'd3', + groupName: 'd3 related packages', + packagePatterns: [ + '(\\b|_)d3(\\b|_)', + ], + }, { groupSlug: 'dedent', groupName: 'dedent related packages', @@ -353,6 +218,22 @@ '@types/delete-empty', ], }, + { + groupSlug: 'dragselect', + groupName: 'dragselect related packages', + packageNames: [ + 'dragselect', + '@types/dragselect', + ], + labels: [ + 'release_note:skip', + 'Team:Operations', + 'renovate', + 'v8.0.0', + 'v7.6.0', + ':ml', + ], + }, { groupSlug: 'elasticsearch', groupName: 'elasticsearch related packages', @@ -361,6 +242,21 @@ '@types/elasticsearch', ], }, + { + groupSlug: 'eslint', + groupName: 'eslint related packages', + packagePatterns: [ + '(\\b|_)eslint(\\b|_)', + ], + }, + { + groupSlug: 'fancy-log', + groupName: 'fancy-log related packages', + packageNames: [ + 'fancy-log', + '@types/fancy-log', + ], + }, { groupSlug: 'fetch-mock', groupName: 'fetch-mock related packages', @@ -369,6 +265,14 @@ '@types/fetch-mock', ], }, + { + groupSlug: 'file-saver', + groupName: 'file-saver related packages', + packageNames: [ + 'file-saver', + '@types/file-saver', + ], + }, { groupSlug: 'getopts', groupName: 'getopts related packages', @@ -377,6 +281,22 @@ '@types/getopts', ], }, + { + groupSlug: 'getos', + groupName: 'getos related packages', + packageNames: [ + 'getos', + '@types/getos', + ], + }, + { + groupSlug: 'git-url-parse', + groupName: 'git-url-parse related packages', + packageNames: [ + 'git-url-parse', + '@types/git-url-parse', + ], + }, { groupSlug: 'glob', groupName: 'glob related packages', @@ -394,387 +314,395 @@ ], }, { - groupSlug: 'has-ansi', - groupName: 'has-ansi related packages', - packageNames: [ - 'has-ansi', - '@types/has-ansi', + groupSlug: 'graphql', + groupName: 'graphql related packages', + packagePatterns: [ + '(\\b|_)graphql(\\b|_)', + '(\\b|_)apollo(\\b|_)', ], }, { - groupSlug: 'history', - groupName: 'history related packages', - packageNames: [ - 'history', - '@types/history', + groupSlug: 'grunt', + groupName: 'grunt related packages', + packagePatterns: [ + '(\\b|_)grunt(\\b|_)', ], }, { - groupSlug: 'jquery', - groupName: 'jquery related packages', - packageNames: [ - 'jquery', - '@types/jquery', + groupSlug: 'gulp', + groupName: 'gulp related packages', + packagePatterns: [ + '(\\b|_)gulp(\\b|_)', ], }, { - groupSlug: 'js-yaml', - groupName: 'js-yaml related packages', - packageNames: [ - 'js-yaml', - '@types/js-yaml', + groupSlug: 'hapi', + groupName: 'hapi related packages', + packagePatterns: [ + '(\\b|_)hapi(\\b|_)', ], - }, - { - groupSlug: 'json5', - groupName: 'json5 related packages', packageNames: [ - 'json5', - '@types/json5', + 'hapi', + '@types/hapi', + 'joi', + '@types/joi', + 'boom', + '@types/boom', + 'hoek', + '@types/hoek', + 'h2o2', + '@types/h2o2', + '@elastic/good', + '@types/elastic__good', + 'good-squeeze', + '@types/good-squeeze', + 'inert', + '@types/inert', ], }, { - groupSlug: 'license-checker', - groupName: 'license-checker related packages', + groupSlug: 'has-ansi', + groupName: 'has-ansi related packages', packageNames: [ - 'license-checker', - '@types/license-checker', + 'has-ansi', + '@types/has-ansi', ], }, { - groupSlug: 'listr', - groupName: 'listr related packages', + groupSlug: 'history', + groupName: 'history related packages', packageNames: [ - 'listr', - '@types/listr', + 'history', + '@types/history', ], }, { - groupSlug: 'lodash', - groupName: 'lodash related packages', + groupSlug: 'indent-string', + groupName: 'indent-string related packages', packageNames: [ - 'lodash', - '@types/lodash', + 'indent-string', + '@types/indent-string', ], }, { - groupSlug: 'lru-cache', - groupName: 'lru-cache related packages', + groupSlug: 'intl-relativeformat', + groupName: 'intl-relativeformat related packages', packageNames: [ - 'lru-cache', - '@types/lru-cache', + 'intl-relativeformat', + '@types/intl-relativeformat', ], }, { - groupSlug: 'markdown-it', - groupName: 'markdown-it related packages', - packageNames: [ - 'markdown-it', - '@types/markdown-it', + groupSlug: 'jest', + groupName: 'jest related packages', + packagePatterns: [ + '(\\b|_)jest(\\b|_)', ], }, { - groupSlug: 'minimatch', - groupName: 'minimatch related packages', + groupSlug: 'jquery', + groupName: 'jquery related packages', packageNames: [ - 'minimatch', - '@types/minimatch', + 'jquery', + '@types/jquery', ], }, { - groupSlug: 'mustache', - groupName: 'mustache related packages', + groupSlug: 'js-yaml', + groupName: 'js-yaml related packages', packageNames: [ - 'mustache', - '@types/mustache', + 'js-yaml', + '@types/js-yaml', ], }, { - groupSlug: 'node', - groupName: 'node related packages', + groupSlug: 'jsdom', + groupName: 'jsdom related packages', packageNames: [ - 'node', - '@types/node', + 'jsdom', + '@types/jsdom', ], }, { - groupSlug: 'opn', - groupName: 'opn related packages', + groupSlug: 'json-stable-stringify', + groupName: 'json-stable-stringify related packages', packageNames: [ - 'opn', - '@types/opn', + 'json-stable-stringify', + '@types/json-stable-stringify', ], }, { - groupSlug: 'pngjs', - groupName: 'pngjs related packages', + groupSlug: 'json5', + groupName: 'json5 related packages', packageNames: [ - 'pngjs', - '@types/pngjs', + 'json5', + '@types/json5', ], }, { - groupSlug: 'podium', - groupName: 'podium related packages', + groupSlug: 'jsonwebtoken', + groupName: 'jsonwebtoken related packages', packageNames: [ - 'podium', - '@types/podium', + 'jsonwebtoken', + '@types/jsonwebtoken', ], }, { - groupSlug: '@reach/router', - groupName: '@reach/router related packages', + groupSlug: 'jsts', + groupName: 'jsts related packages', packageNames: [ - '@reach/router', - '@types/reach__router', + 'jsts', + '@types/jsts', ], + allowedVersions: '^1.6.2', }, { - groupSlug: 'request', - groupName: 'request related packages', - packageNames: [ - 'request', - '@types/request', + groupSlug: 'karma', + groupName: 'karma related packages', + packagePatterns: [ + '(\\b|_)karma(\\b|_)', ], }, { - groupSlug: 'selenium-webdriver', - groupName: 'selenium-webdriver related packages', + groupSlug: 'language server', + groupName: 'language server related packages', packageNames: [ - 'selenium-webdriver', - '@types/selenium-webdriver', + 'vscode-jsonrpc', + '@types/vscode-jsonrpc', + 'vscode-languageserver', + '@types/vscode-languageserver', + 'vscode-languageserver-types', + '@types/vscode-languageserver-types', ], }, { - groupSlug: 'semver', - groupName: 'semver related packages', + groupSlug: 'license-checker', + groupName: 'license-checker related packages', packageNames: [ - 'semver', - '@types/semver', + 'license-checker', + '@types/license-checker', ], }, { - groupSlug: 'sinon', - groupName: 'sinon related packages', + groupSlug: 'listr', + groupName: 'listr related packages', packageNames: [ - 'sinon', - '@types/sinon', + 'listr', + '@types/listr', ], }, { - groupSlug: 'strip-ansi', - groupName: 'strip-ansi related packages', + groupSlug: 'lodash', + groupName: 'lodash related packages', packageNames: [ - 'strip-ansi', - '@types/strip-ansi', + 'lodash', + '@types/lodash', ], }, { - groupSlug: 'styled-components', - groupName: 'styled-components related packages', + groupSlug: 'lodash.clonedeep', + groupName: 'lodash.clonedeep related packages', packageNames: [ - 'styled-components', - '@types/styled-components', + 'lodash.clonedeep', + '@types/lodash.clonedeep', ], }, { - groupSlug: 'supertest', - groupName: 'supertest related packages', + groupSlug: 'lodash.clonedeepwith', + groupName: 'lodash.clonedeepwith related packages', packageNames: [ - 'supertest', - '@types/supertest', + 'lodash.clonedeepwith', + '@types/lodash.clonedeepwith', ], }, { - groupSlug: 'supertest-as-promised', - groupName: 'supertest-as-promised related packages', + groupSlug: 'log-symbols', + groupName: 'log-symbols related packages', packageNames: [ - 'supertest-as-promised', - '@types/supertest-as-promised', + 'log-symbols', + '@types/log-symbols', ], }, { - groupSlug: 'type-detect', - groupName: 'type-detect related packages', + groupSlug: 'lru-cache', + groupName: 'lru-cache related packages', packageNames: [ - 'type-detect', - '@types/type-detect', + 'lru-cache', + '@types/lru-cache', ], }, { - groupSlug: 'uuid', - groupName: 'uuid related packages', + groupSlug: 'mapbox-gl', + groupName: 'mapbox-gl related packages', packageNames: [ - 'uuid', - '@types/uuid', + 'mapbox-gl', + '@types/mapbox-gl', ], }, { - groupSlug: 'vinyl-fs', - groupName: 'vinyl-fs related packages', + groupSlug: 'markdown-it', + groupName: 'markdown-it related packages', packageNames: [ - 'vinyl-fs', - '@types/vinyl-fs', + 'markdown-it', + '@types/markdown-it', ], }, { - groupSlug: 'zen-observable', - groupName: 'zen-observable related packages', + groupSlug: 'memoize-one', + groupName: 'memoize-one related packages', packageNames: [ - 'zen-observable', - '@types/zen-observable', + 'memoize-one', + '@types/memoize-one', ], }, { - groupSlug: 'archiver', - groupName: 'archiver related packages', + groupSlug: 'mime', + groupName: 'mime related packages', packageNames: [ - 'archiver', - '@types/archiver', + 'mime', + '@types/mime', ], }, { - groupSlug: 'base64-js', - groupName: 'base64-js related packages', + groupSlug: 'minimatch', + groupName: 'minimatch related packages', packageNames: [ - 'base64-js', - '@types/base64-js', + 'minimatch', + '@types/minimatch', ], }, { - groupSlug: 'chroma-js', - groupName: 'chroma-js related packages', - packageNames: [ - 'chroma-js', - '@types/chroma-js', + groupSlug: 'mocha', + groupName: 'mocha related packages', + packagePatterns: [ + '(\\b|_)mocha(\\b|_)', ], }, { - groupSlug: 'color', - groupName: 'color related packages', - packageNames: [ - 'color', - '@types/color', + groupSlug: 'moment', + groupName: 'moment related packages', + packagePatterns: [ + '(\\b|_)moment(\\b|_)', ], }, { - groupSlug: 'cytoscape', - groupName: 'cytoscape related packages', + groupSlug: 'mustache', + groupName: 'mustache related packages', packageNames: [ - 'cytoscape', - '@types/cytoscape', + 'mustache', + '@types/mustache', ], }, { - groupSlug: 'fancy-log', - groupName: 'fancy-log related packages', + groupSlug: 'ncp', + groupName: 'ncp related packages', packageNames: [ - 'fancy-log', - '@types/fancy-log', + 'ncp', + '@types/ncp', ], }, { - groupSlug: 'file-saver', - groupName: 'file-saver related packages', + groupSlug: 'nock', + groupName: 'nock related packages', packageNames: [ - 'file-saver', - '@types/file-saver', + 'nock', + '@types/nock', ], }, { - groupSlug: 'getos', - groupName: 'getos related packages', + groupSlug: 'node', + groupName: 'node related packages', packageNames: [ - 'getos', - '@types/getos', + 'node', + '@types/node', ], }, { - groupSlug: 'git-url-parse', - groupName: 'git-url-parse related packages', + groupSlug: 'node-fetch', + groupName: 'node-fetch related packages', packageNames: [ - 'git-url-parse', - '@types/git-url-parse', + 'node-fetch', + '@types/node-fetch', ], }, { - groupSlug: 'jsdom', - groupName: 'jsdom related packages', + groupSlug: 'node-forge', + groupName: 'node-forge related packages', packageNames: [ - 'jsdom', - '@types/jsdom', + 'node-forge', + '@types/node-forge', ], }, { - groupSlug: 'jsonwebtoken', - groupName: 'jsonwebtoken related packages', + groupSlug: 'nodemailer', + groupName: 'nodemailer related packages', packageNames: [ - 'jsonwebtoken', - '@types/jsonwebtoken', + 'nodemailer', + '@types/nodemailer', ], }, { - groupSlug: 'mapbox-gl', - groupName: 'mapbox-gl related packages', + groupSlug: 'object-hash', + groupName: 'object-hash related packages', packageNames: [ - 'mapbox-gl', - '@types/mapbox-gl', + 'object-hash', + '@types/object-hash', ], }, { - groupSlug: 'memoize-one', - groupName: 'memoize-one related packages', + groupSlug: 'opn', + groupName: 'opn related packages', packageNames: [ - 'memoize-one', - '@types/memoize-one', + 'opn', + '@types/opn', ], }, { - groupSlug: 'mime', - groupName: 'mime related packages', + groupSlug: 'ora', + groupName: 'ora related packages', packageNames: [ - 'mime', - '@types/mime', + 'ora', + '@types/ora', ], }, { - groupSlug: 'nock', - groupName: 'nock related packages', + groupSlug: 'papaparse', + groupName: 'papaparse related packages', packageNames: [ - 'nock', - '@types/nock', + 'papaparse', + '@types/papaparse', ], }, { - groupSlug: 'node-fetch', - groupName: 'node-fetch related packages', + groupSlug: 'parse-link-header', + groupName: 'parse-link-header related packages', packageNames: [ - 'node-fetch', - '@types/node-fetch', + 'parse-link-header', + '@types/parse-link-header', ], }, { - groupSlug: 'nodemailer', - groupName: 'nodemailer related packages', + groupSlug: 'pegjs', + groupName: 'pegjs related packages', packageNames: [ - 'nodemailer', - '@types/nodemailer', + 'pegjs', + '@types/pegjs', ], }, { - groupSlug: 'object-hash', - groupName: 'object-hash related packages', + groupSlug: 'pngjs', + groupName: 'pngjs related packages', packageNames: [ - 'object-hash', - '@types/object-hash', + 'pngjs', + '@types/pngjs', ], }, { - groupSlug: 'papaparse', - groupName: 'papaparse related packages', + groupSlug: 'podium', + groupName: 'podium related packages', packageNames: [ - 'papaparse', - '@types/papaparse', + 'podium', + '@types/podium', ], }, { @@ -793,6 +721,35 @@ '@types/puppeteer', ], }, + { + groupSlug: 'react', + groupName: 'react related packages', + packagePatterns: [ + '(\\b|_)react(\\b|_)', + '(\\b|_)redux(\\b|_)', + '(\\b|_)enzyme(\\b|_)', + ], + packageNames: [ + 'ngreact', + '@types/ngreact', + 'recompose', + '@types/recompose', + 'prop-types', + '@types/prop-types', + 'typescript-fsa-reducers', + '@types/typescript-fsa-reducers', + 'reselect', + '@types/reselect', + ], + }, + { + groupSlug: 'read-pkg', + groupName: 'read-pkg related packages', + packageNames: [ + 'read-pkg', + '@types/read-pkg', + ], + }, { groupSlug: 'reduce-reducers', groupName: 'reduce-reducers related packages', @@ -802,123 +759,166 @@ ], }, { - groupSlug: 'tar-fs', - groupName: 'tar-fs related packages', + groupSlug: 'request', + groupName: 'request related packages', packageNames: [ - 'tar-fs', - '@types/tar-fs', + 'request', + '@types/request', ], }, { - groupSlug: 'tinycolor2', - groupName: 'tinycolor2 related packages', + groupSlug: 'selenium-webdriver', + groupName: 'selenium-webdriver related packages', packageNames: [ - 'tinycolor2', - '@types/tinycolor2', + 'selenium-webdriver', + '@types/selenium-webdriver', ], }, { - groupSlug: 'xml-crypto', - groupName: 'xml-crypto related packages', + groupSlug: 'semver', + groupName: 'semver related packages', packageNames: [ - 'xml-crypto', - '@types/xml-crypto', + 'semver', + '@types/semver', ], }, { - groupSlug: 'xml2js', - groupName: 'xml2js related packages', + groupSlug: 'sinon', + groupName: 'sinon related packages', packageNames: [ - 'xml2js', - '@types/xml2js', + 'sinon', + '@types/sinon', ], }, { - groupSlug: 'intl-relativeformat', - groupName: 'intl-relativeformat related packages', + groupSlug: 'storybook', + groupName: 'storybook related packages', + packagePatterns: [ + '(\\b|_)storybook(\\b|_)', + ], + }, + { + groupSlug: 'strip-ansi', + groupName: 'strip-ansi related packages', packageNames: [ - 'intl-relativeformat', - '@types/intl-relativeformat', + 'strip-ansi', + '@types/strip-ansi', ], }, { - groupSlug: 'cmd-shim', - groupName: 'cmd-shim related packages', + groupSlug: 'strong-log-transformer', + groupName: 'strong-log-transformer related packages', packageNames: [ - 'cmd-shim', - '@types/cmd-shim', + 'strong-log-transformer', + '@types/strong-log-transformer', ], }, { - groupSlug: 'cpy', - groupName: 'cpy related packages', + groupSlug: 'styled-components', + groupName: 'styled-components related packages', packageNames: [ - 'cpy', - '@types/cpy', + 'styled-components', + '@types/styled-components', ], }, { - groupSlug: 'indent-string', - groupName: 'indent-string related packages', + groupSlug: 'supertest', + groupName: 'supertest related packages', packageNames: [ - 'indent-string', - '@types/indent-string', + 'supertest', + '@types/supertest', ], }, { - groupSlug: 'lodash.clonedeepwith', - groupName: 'lodash.clonedeepwith related packages', + groupSlug: 'supertest-as-promised', + groupName: 'supertest-as-promised related packages', packageNames: [ - 'lodash.clonedeepwith', - '@types/lodash.clonedeepwith', + 'supertest-as-promised', + '@types/supertest-as-promised', ], }, { - groupSlug: 'log-symbols', - groupName: 'log-symbols related packages', + groupSlug: 'tar-fs', + groupName: 'tar-fs related packages', packageNames: [ - 'log-symbols', - '@types/log-symbols', + 'tar-fs', + '@types/tar-fs', ], }, { - groupSlug: 'ncp', - groupName: 'ncp related packages', + groupSlug: 'tempy', + groupName: 'tempy related packages', packageNames: [ - 'ncp', - '@types/ncp', + 'tempy', + '@types/tempy', ], }, { - groupSlug: 'ora', - groupName: 'ora related packages', + groupSlug: 'tinycolor2', + groupName: 'tinycolor2 related packages', packageNames: [ - 'ora', - '@types/ora', + 'tinycolor2', + '@types/tinycolor2', ], }, { - groupSlug: 'read-pkg', - groupName: 'read-pkg related packages', + groupSlug: 'type-detect', + groupName: 'type-detect related packages', packageNames: [ - 'read-pkg', - '@types/read-pkg', + 'type-detect', + '@types/type-detect', ], }, { - groupSlug: 'strong-log-transformer', - groupName: 'strong-log-transformer related packages', + groupSlug: 'typescript', + groupName: 'typescript related packages', + packagePatterns: [ + '(\\b|_)ts(\\b|_)', + '(\\b|_)typescript(\\b|_)', + ], packageNames: [ - 'strong-log-transformer', - '@types/strong-log-transformer', + 'tslib', + '@types/tslib', ], }, { - groupSlug: 'tempy', - groupName: 'tempy related packages', + groupSlug: 'uuid', + groupName: 'uuid related packages', packageNames: [ - 'tempy', - '@types/tempy', + 'uuid', + '@types/uuid', + ], + }, + { + groupSlug: 'vega', + groupName: 'vega related packages', + packagePatterns: [ + '(\\b|_)vega(\\b|_)', + ], + enabled: false, + }, + { + groupSlug: 'vinyl-fs', + groupName: 'vinyl-fs related packages', + packageNames: [ + 'vinyl-fs', + '@types/vinyl-fs', + ], + }, + { + groupSlug: 'webpack', + groupName: 'webpack related packages', + packagePatterns: [ + '(\\b|_)webpack(\\b|_)', + '(\\b|_)loader(\\b|_)', + '(\\b|_)acorn(\\b|_)', + '(\\b|_)terser(\\b|_)', + ], + packageNames: [ + 'mini-css-extract-plugin', + '@types/mini-css-extract-plugin', + 'chokidar', + '@types/chokidar', ], }, { @@ -938,11 +938,27 @@ ], }, { - groupSlug: 'parse-link-header', - groupName: 'parse-link-header related packages', + groupSlug: 'xml-crypto', + groupName: 'xml-crypto related packages', packageNames: [ - 'parse-link-header', - '@types/parse-link-header', + 'xml-crypto', + '@types/xml-crypto', + ], + }, + { + groupSlug: 'xml2js', + groupName: 'xml2js related packages', + packageNames: [ + 'xml2js', + '@types/xml2js', + ], + }, + { + groupSlug: 'zen-observable', + groupName: 'zen-observable related packages', + packageNames: [ + 'zen-observable', + '@types/zen-observable', ], }, { diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 4472891e580fb..fc88f2657018f 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -17,12 +17,23 @@ * under the License. */ -require('../src/setup_node_env'); -require('@kbn/test').runTestsCli([ +// eslint-disable-next-line no-restricted-syntax +const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), - require.resolve('../test/api_integration/config.js'), require.resolve('../test/plugin_functional/config.js'), - require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), +]; +// eslint-disable-next-line no-restricted-syntax +const onlyNotInCoverageTests = [ + require.resolve('../test/api_integration/config.js'), + require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/examples/config.js'), +]; + +require('../src/setup_node_env'); +require('@kbn/test').runTestsCli([ + // eslint-disable-next-line no-restricted-syntax + ...alwaysImportedTests, + // eslint-disable-next-line no-restricted-syntax + ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests), ]); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 6b13d0dc32d3f..9cf5691b88399 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -102,6 +102,8 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { } ensureNotDefined('server.ssl.certificate'); ensureNotDefined('server.ssl.key'); + ensureNotDefined('server.ssl.keystore.path'); + ensureNotDefined('server.ssl.truststore.path'); ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); const elasticsearchHosts = ( @@ -119,6 +121,8 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { }); set('server.ssl.enabled', true); + // TODO: change this cert/key to KBN_CERT_PATH and KBN_KEY_PATH from '@kbn/dev-utils'; will require some work to avoid breaking + // functional tests. Once that is done, the existing test cert/key at DEV_SSL_CERT_PATH and DEV_SSL_KEY_PATH can be deleted. set('server.ssl.certificate', DEV_SSL_CERT_PATH); set('server.ssl.key', DEV_SSL_KEY_PATH); set('elasticsearch.hosts', elasticsearchHosts); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 1c78de966c46f..173d73ffab664 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -55,6 +55,7 @@ - [Provide Legacy Platform API to the New platform plugin](#provide-legacy-platform-api-to-the-new-platform-plugin) - [On the server side](#on-the-server-side) - [On the client side](#on-the-client-side) + - [Updates an application navlink at runtime](#updates-an-app-navlink-at-runtime) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -1194,6 +1195,7 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS | `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | | `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | | `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | +| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.createClient`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md) | | | `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | | | `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | | | `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | | @@ -1623,3 +1625,31 @@ class MyPlugin { It's not currently possible to use a similar pattern on the client-side. Because Legacy platform plugins heavily rely on global angular modules, which aren't available on the new platform. So you can utilize the same approach for only *stateless Angular components*, as long as they are not consumed by a New Platform application. When New Platform applications are on the page, no legacy code is executed, so the `registerLegacyAPI` function would not be called. + +### Updates an application navlink at runtime + +The application API now provides a way to updates some of a registered application's properties after registration. + +```typescript +// inside your plugin's setup function +export class MyPlugin implements Plugin { + private appUpdater = new BehaviorSubject(() => ({})); + setup({ application }) { + application.register({ + id: 'my-app', + title: 'My App', + updater$: this.appUpdater, + async mount(params) { + const { renderApp } = await import('./application'); + return renderApp(params); + }, + }); + } + start() { + // later, when the navlink needs to be updated + appUpdater.next(() => { + navLinkStatus: AppNavLinkStatus.disabled, + tooltip: 'Application disabled', + }) + } +``` \ No newline at end of file diff --git a/src/core/README.md b/src/core/README.md index 8863658e0040c..f4539191d448b 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -7,6 +7,7 @@ Core Plugin API Documentation: - [Core Public API](/docs/development/core/public/kibana-plugin-public.md) - [Core Server API](/docs/development/core/server/kibana-plugin-server.md) - [Conventions for Plugins](./CONVENTIONS.md) + - [Testing Kibana Plugins](./TESTING.md) - [Migration guide for porting existing plugins](./MIGRATION.md) Internal Documentation: diff --git a/src/core/TESTING.md b/src/core/TESTING.md new file mode 100644 index 0000000000000..6139820d02a14 --- /dev/null +++ b/src/core/TESTING.md @@ -0,0 +1,254 @@ +# Testing Kibana Plugins + +This document outlines best practices and patterns for testing Kibana Plugins. + +- [Strategy](#strategy) +- [Core Integrations](#core-integrations) + - [Core Mocks](#core-mocks) + - [Strategies for specific Core APIs](#strategies-for-specific-core-apis) + - [HTTP Routes](#http-routes) + - [SavedObjects](#savedobjects) + - [Elasticsearch](#elasticsearch) +- [Plugin Integrations](#plugin-integrations) +- [Plugin Contracts](#plugin-contracts) + +## Strategy + +In general, we recommend three tiers of tests: +- Unit tests: small, fast, exhaustive, make heavy use of mocks for external dependencies +- Integration tests: higher-level tests that verify interactions between systems (eg. HTTP APIs, Elasticsearch API calls, calling other plugin contracts). +- End-to-end tests (e2e): tests that verify user-facing behavior through the browser + +These tiers should roughly follow the traditional ["testing pyramid"](https://martinfowler.com/articles/practical-test-pyramid.html), where there are more exhaustive testing at the unit level, fewer at the integration level, and very few at the functional level. + +## New concerns in the Kibana Platform + +The Kibana Platform introduces new concepts that legacy plugins did not have concern themselves with. Namely: +- **Lifecycles**: plugins now have explicit lifecycle methods that must interop with Core APIs and other plugins. +- **Shared runtime**: plugins now all run in the same process at the same time. On the frontend, this is different behavior than the legacy plugins. Developers should take care not to break other plugins when interacting with their enviornment (Node.js or Browser). +- **Single page application**: Kibana's frontend is now a single-page application where all plugins are running, but only one application is mounted at a time. Plugins need to handle mounting and unmounting, cleanup, and avoid overriding global browser behaviors in this shared space. +- **Dependency management**: plugins must now explicitly declare their dependencies on other plugins, both required and optional. Plugins should ensure to test conditions where a optional dependency is missing. + +Simply porting over existing tests when migrating your plugin to the Kibana Platform will leave blind spots in test coverage. It is highly recommended that plugins add new tests that cover these new concerns. + +## Core Integrations + +### Core Mocks + +When testing a plugin's integration points with Core APIs, it is heavily recommended to utilize the mocks provided in `src/core/server/mocks` and `src/core/public/mocks`. The majority of these mocks are dumb `jest` mocks that mimic the interface of their respective Core APIs, however they do not return realistic return values. + +If the unit under test expects a particular response from a Core API, the test will need to set this return value explicitly. The return values are type checked to match the Core API where possible to ensure that mocks are updated when Core APIs changed. + +#### Example + +```ts +import { elasticsearchServiceMock } from 'src/core/server/mocks'; + +test('my test', async () => { + // Setup mock and faked response + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.callAsCurrentUser.mockResolvedValue(/** insert ES response here */); + + // Call unit under test with mocked client + const result = await myFunction(esClient); + + // Assert that client was called with expected arguments + expect(esClient.callAsCurrentUser).toHaveBeenCalledWith(/** expected args */); + // Expect that unit under test returns expected value based on client's response + expect(result).toEqual(/** expected return value */) +}); +``` + +### Strategies for specific Core APIs + +#### HTTP Routes + +_How to test route handlers_ + +### Applications + +Kibana Platform applications have less control over the page than legacy applications did. It is important that your app is built to handle it's co-habitance with other plugins in the browser. Applications are mounted and unmounted from the DOM as the user navigates between them, without full-page refreshes, as a single-page application (SPA). + +These long-lived sessions make cleanup more important than before. It's entirely possible a user has a single browsing session open for weeks at a time, without ever doing a full-page refresh. Common things that need to be cleaned up (and tested!) when your application is unmounted: +- Subscriptions and polling (eg. `uiSettings.get$()`) +- Any Core API calls that set state (eg. `core.chrome.setIsVisible`). +- Open connections (eg. a Websocket) + +While applications do get an opportunity to unmount and run cleanup logic, it is also important that you do not _depend_ on this logic to run. The browser tab may get closed without running cleanup logic, so it is not guaranteed to be run. For instance, you should not depend on unmounting logic to run in order to save state to `localStorage` or to the backend. + +#### Example + +By following the [renderApp](./CONVENTIONS.md#applications) convention, you can greatly reduce the amount of logic in your application's mount function. This makes testing your application's actual rendering logic easier. + +```tsx +/** public/plugin.ts */ +class Plugin { + setup(core) { + core.application.register({ + // id, title, etc. + async mount(params) { + const [{ renderApp }, [coreStart, startDeps]] = await Promise.all([ + import('./application'), + core.getStartServices() + ]); + + return renderApp(params, coreStart, startDeps); + } + }) + } +} +``` + +We _could_ still write tests for this logic, but you may find that you're just asserting the same things that would be covered by type-checks. + +
+See example + +```ts +/** public/plugin.test.ts */ +jest.mock('./application', () => ({ renderApp: jest.fn() })); +import { coreMock } from 'src/core/public/mocks'; +import { renderApp: renderAppMock } from './application'; +import { Plugin } from './plugin'; + +describe('Plugin', () => { + it('registers an app', () => { + const coreSetup = coreMock.createSetup(); + new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); + expect(coreSetup.application.register).toHaveBeenCalledWith({ + id: 'myApp', + mount: expect.any(Function) + }); + }); + + // Test the glue code from Plugin -> renderApp + it('application.mount wires up dependencies to renderApp', async () => { + const coreSetup = coreMock.createSetup(); + const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); + const unmountMock = jest.fn(); + renderAppMock.mockReturnValue(unmountMock); + const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + + new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); + // Grab registered mount function + const mount = coreSetup.application.register.mock.calls[0][0].mount; + + const unmount = await mount(params); + expect(renderAppMock).toHaveBeenCalledWith(params, coreStartMock, startDepsMock); + expect(unmount).toBe(unmountMock); + }); +}); +``` + +
+ +The more interesting logic is in `renderApp`: + +```ts +/** public/application.ts */ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { AppMountParams, CoreStart } from 'src/core/public'; +import { AppRoot } from './components/app_root'; + +export const renderApp = ({ element, appBasePath }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { + // Hide the chrome while this app is mounted for a full screen experience + core.chrome.setIsVisible(false); + + // uiSettings subscription + const uiSettingsClient = core.uiSettings.client; + const pollingSubscription = uiSettingClient.get$('mysetting1').subscribe(async mySetting1 => { + const value = core.http.fetch(/** use `mySetting1` in request **/); + // ... + }); + + // Render app + ReactDOM.render( + , + element + ); + + return () => { + // Unmount UI + ReactDOM.unmountComponentAtNode(element); + // Close any subscriptions + pollingSubscription.unsubscribe(); + // Make chrome visible again + core.chrome.setIsVisible(true); + }; +}; +``` + +In testing `renderApp` you should be verifying that: +1) Your application mounts and unmounts correctly +2) Cleanup logic is completed as expected + +```ts +/** public/application.test.ts */ +import { coreMock } from 'src/core/public/mocks'; +import { renderApp } from './application'; + +describe('renderApp', () => { + it('mounts and unmounts UI', () => { + const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const core = coreMock.createStart(); + + // Verify some expected DOM element is rendered into the element + const unmount = renderApp(params, core, {}); + expect(params.element.querySelector('.some-app-class')).not.toBeUndefined(); + // Verify the element is empty after unmounting + unmount(); + expect(params.element.innerHTML).toEqual(''); + }); + + it('unsubscribes from uiSettings', () => { + const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const core = coreMock.createStart(); + // Create a fake Subject you can use to monitor observers + const settings$ = new Subject(); + core.uiSettings.get$.mockReturnValue(settings$); + + // Verify mounting adds an observer + const unmount = renderApp(params, core, {}); + expect(settings$.observers.length).toBe(1); + // Verify no observers remaining after unmount is called + unmount(); + expect(settings$.observers.length).toBe(0); + }); + + it('resets chrome visibility', () => { + const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const core = coreMock.createStart(); + + // Verify stateful Core API was called on mount + const unmount = renderApp(params, core, {}); + expect(core.chrome.setIsVisible).toHaveBeenCalledWith(false); + core.chrome.setIsVisible.mockClear(); // reset mock + // Verify stateful Core API was called on unmount + unmount(); + expect(core.chrome.setIsVisible).toHaveBeenCalledWith(true); + }) +}); +``` + +#### SavedObjects + +_How to test SO operations_ + +#### Elasticsearch + +_How to test ES clients_ + +## Plugin Integrations + +_How to test against specific plugin APIs (eg. data plugin)_ + +## Plugin Contracts + +_How to test your plugin's exposed API_ + +Guidelines: +- Plugins should never interact with other plugins' REST API directly +- Plugins should interact with other plugins via JavaScript contracts +- Exposed contracts need to be well tested to ensure breaking changes are detected easily diff --git a/src/core/public/application/application_leave.test.ts b/src/core/public/application/application_leave.test.ts new file mode 100644 index 0000000000000..e06183d8bb8d9 --- /dev/null +++ b/src/core/public/application/application_leave.test.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isConfirmAction, getLeaveAction } from './application_leave'; +import { AppLeaveActionType } from './types'; + +describe('isConfirmAction', () => { + it('returns true if action is confirm', () => { + expect(isConfirmAction({ type: AppLeaveActionType.confirm, text: 'message' })).toEqual(true); + }); + it('returns false if action is default', () => { + expect(isConfirmAction({ type: AppLeaveActionType.default })).toEqual(false); + }); +}); + +describe('getLeaveAction', () => { + it('returns the default action provided by the handler', () => { + expect(getLeaveAction(actions => actions.default())).toEqual({ + type: AppLeaveActionType.default, + }); + }); + it('returns the confirm action provided by the handler', () => { + expect(getLeaveAction(actions => actions.confirm('some message'))).toEqual({ + type: AppLeaveActionType.confirm, + text: 'some message', + }); + expect(getLeaveAction(actions => actions.confirm('another message', 'a title'))).toEqual({ + type: AppLeaveActionType.confirm, + text: 'another message', + title: 'a title', + }); + }); +}); diff --git a/src/core/public/application/application_leave.tsx b/src/core/public/application/application_leave.tsx new file mode 100644 index 0000000000000..7b69d70d3f6f6 --- /dev/null +++ b/src/core/public/application/application_leave.tsx @@ -0,0 +1,46 @@ +/* + * 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 { + AppLeaveActionFactory, + AppLeaveActionType, + AppLeaveAction, + AppLeaveConfirmAction, + AppLeaveHandler, +} from './types'; + +const appLeaveActionFactory: AppLeaveActionFactory = { + confirm(text: string, title?: string) { + return { type: AppLeaveActionType.confirm, text, title }; + }, + default() { + return { type: AppLeaveActionType.default }; + }, +}; + +export function isConfirmAction(action: AppLeaveAction): action is AppLeaveConfirmAction { + return action.type === AppLeaveActionType.confirm; +} + +export function getLeaveAction(handler?: AppLeaveHandler): AppLeaveAction { + if (!handler) { + return appLeaveActionFactory.default(); + } + return handler(appLeaveActionFactory); +} diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index b2e2161c92cc8..dee47315fc322 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { @@ -25,17 +25,21 @@ import { InternalApplicationStart, ApplicationStart, InternalApplicationSetup, + App, + LegacyApp, } from './types'; import { ApplicationServiceContract } from './test_types'; const createSetupContractMock = (): jest.Mocked => ({ register: jest.fn(), + registerAppUpdater: jest.fn(), registerMountContext: jest.fn(), }); const createInternalSetupContractMock = (): jest.Mocked => ({ register: jest.fn(), registerLegacyApp: jest.fn(), + registerAppUpdater: jest.fn(), registerMountContext: jest.fn(), }); @@ -50,8 +54,7 @@ const createInternalStartContractMock = (): jest.Mocked(); return { - availableApps: new Map(), - availableLegacyApps: new Map(), + applications$: new BehaviorSubject>(new Map()), capabilities: capabilitiesServiceMock.createStartContract().capabilities, currentAppId$: currentAppId$.asObservable(), getComponent: jest.fn(), diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index d064b17ace142..4672a42c9eb06 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -18,24 +18,42 @@ */ import { createElement } from 'react'; -import { Subject } from 'rxjs'; -import { bufferCount, skip, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { bufferCount, skip, take, takeUntil } from 'rxjs/operators'; import { shallow } from 'enzyme'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; +import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; - -function mount() {} +import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types'; + +const createApp = (props: Partial): App => { + return { + id: 'some-id', + title: 'some-title', + mount: () => () => undefined, + ...props, + }; +}; + +const createLegacyApp = (props: Partial): LegacyApp => { + return { + id: 'some-id', + title: 'some-title', + appUrl: '/my-url', + ...props, + }; +}; + +let setupDeps: MockLifecycle<'setup'>; +let startDeps: MockLifecycle<'start'>; +let service: ApplicationService; describe('#setup()', () => { - let setupDeps: MockLifecycle<'setup'>; - let startDeps: MockLifecycle<'start'>; - let service: ApplicationService; - beforeEach(() => { const http = httpServiceMock.createSetupContract({ basePath: '/test' }); setupDeps = { @@ -44,7 +62,7 @@ describe('#setup()', () => { injectedMetadata: injectedMetadataServiceMock.createSetupContract(), }; setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); - startDeps = { http, injectedMetadata: setupDeps.injectedMetadata }; + startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); }); @@ -52,9 +70,9 @@ describe('#setup()', () => { it('throws an error if two apps with the same id are registered', () => { const { register } = service.setup(setupDeps); - register(Symbol(), { id: 'app1', mount } as any); + register(Symbol(), createApp({ id: 'app1' })); expect(() => - register(Symbol(), { id: 'app1', mount } as any) + register(Symbol(), createApp({ id: 'app1' })) ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the id \\"app1\\""` ); @@ -65,37 +83,91 @@ describe('#setup()', () => { await service.start(startDeps); expect(() => - register(Symbol(), { id: 'app1', mount } as any) + register(Symbol(), createApp({ id: 'app1' })) ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); + it('allows to register a statusUpdater for the application', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + const updater$ = new BehaviorSubject(app => ({})); + setup.register(pluginId, createApp({ id: 'app1', updater$ })); + setup.register(pluginId, createApp({ id: 'app2' })); + const { applications$ } = await service.start(startDeps); + + let applications = await applications$.pipe(take(1)).toPromise(); + expect(applications.size).toEqual(2); + expect(applications.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }) + ); + expect(applications.get('app2')).toEqual( + expect.objectContaining({ + id: 'app2', + legacy: false, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }) + ); + + updater$.next(app => ({ + status: AppStatus.inaccessible, + tooltip: 'App inaccessible due to reason', + })); + + applications = await applications$.pipe(take(1)).toPromise(); + expect(applications.size).toEqual(2); + expect(applications.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.inaccessible, + tooltip: 'App inaccessible due to reason', + }) + ); + expect(applications.get('app2')).toEqual( + expect.objectContaining({ + id: 'app2', + legacy: false, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }) + ); + }); + it('throws an error if an App with the same appRoute is registered', () => { const { register, registerLegacyApp } = service.setup(setupDeps); - register(Symbol(), { id: 'app1', mount } as any); + register(Symbol(), createApp({ id: 'app1' })); expect(() => - register(Symbol(), { id: 'app2', mount, appRoute: '/app/app1' } as any) + register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app1' })) ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the appRoute \\"/app/app1\\""` ); - expect(() => registerLegacyApp({ id: 'app1' } as any)).not.toThrow(); + expect(() => registerLegacyApp(createLegacyApp({ id: 'app1' }))).toThrow(); - register(Symbol(), { id: 'app-next', mount, appRoute: '/app/app3' } as any); + register(Symbol(), createApp({ id: 'app-next', appRoute: '/app/app3' })); expect(() => - register(Symbol(), { id: 'app2', mount, appRoute: '/app/app3' } as any) + register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app3' })) ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the appRoute \\"/app/app3\\""` ); - expect(() => registerLegacyApp({ id: 'app3' } as any)).not.toThrow(); + expect(() => registerLegacyApp(createLegacyApp({ id: 'app3' }))).not.toThrow(); }); it('throws an error if an App starts with the HTTP base path', () => { const { register } = service.setup(setupDeps); expect(() => - register(Symbol(), { id: 'app2', mount, appRoute: '/test/app2' } as any) + register(Symbol(), createApp({ id: 'app2', appRoute: '/test/app2' })) ).toThrowErrorMatchingInlineSnapshot( `"Cannot register an application route that includes HTTP base path"` ); @@ -106,9 +178,11 @@ describe('#setup()', () => { it('throws an error if two apps with the same id are registered', () => { const { registerLegacyApp } = service.setup(setupDeps); - registerLegacyApp({ id: 'app2' } as any); - expect(() => registerLegacyApp({ id: 'app2' } as any)).toThrowErrorMatchingInlineSnapshot( - `"A legacy application is already registered with the id \\"app2\\""` + registerLegacyApp(createLegacyApp({ id: 'app2' })); + expect(() => + registerLegacyApp(createLegacyApp({ id: 'app2' })) + ).toThrowErrorMatchingInlineSnapshot( + `"An application is already registered with the id \\"app2\\""` ); }); @@ -116,22 +190,228 @@ describe('#setup()', () => { const { registerLegacyApp } = service.setup(setupDeps); await service.start(startDeps); - expect(() => registerLegacyApp({ id: 'app2' } as any)).toThrowErrorMatchingInlineSnapshot( - `"Applications cannot be registered after \\"setup\\""` - ); + expect(() => + registerLegacyApp(createLegacyApp({ id: 'app2' })) + ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); it('throws an error if a LegacyApp with the same appRoute is registered', () => { const { register, registerLegacyApp } = service.setup(setupDeps); - registerLegacyApp({ id: 'app1' } as any); + registerLegacyApp(createLegacyApp({ id: 'app1' })); expect(() => - register(Symbol(), { id: 'app2', mount, appRoute: '/app/app1' } as any) + register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app1' })) ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the appRoute \\"/app/app1\\""` ); - expect(() => registerLegacyApp({ id: 'app1:other' } as any)).not.toThrow(); + expect(() => registerLegacyApp(createLegacyApp({ id: 'app1:other' }))).not.toThrow(); + }); + }); + + describe('registerAppStatusUpdater', () => { + it('updates status fields', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + setup.register(pluginId, createApp({ id: 'app1' })); + setup.register(pluginId, createApp({ id: 'app2' })); + setup.registerAppUpdater( + new BehaviorSubject(app => { + if (app.id === 'app1') { + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.disabled, + tooltip: 'App inaccessible due to reason', + }; + } + return { + tooltip: 'App accessible', + }; + }) + ); + const start = await service.start(startDeps); + const applications = await start.applications$.pipe(take(1)).toPromise(); + + expect(applications.size).toEqual(2); + expect(applications.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + navLinkStatus: AppNavLinkStatus.disabled, + status: AppStatus.inaccessible, + tooltip: 'App inaccessible due to reason', + }) + ); + expect(applications.get('app2')).toEqual( + expect.objectContaining({ + id: 'app2', + legacy: false, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + tooltip: 'App accessible', + }) + ); + }); + + it(`properly combine with application's updater$`, async () => { + const setup = service.setup(setupDeps); + const pluginId = Symbol('plugin'); + const appStatusUpdater$ = new BehaviorSubject(app => ({ + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.disabled, + })); + setup.register(pluginId, createApp({ id: 'app1', updater$: appStatusUpdater$ })); + setup.register(pluginId, createApp({ id: 'app2' })); + + setup.registerAppUpdater( + new BehaviorSubject(app => { + if (app.id === 'app1') { + return { + status: AppStatus.accessible, + tooltip: 'App inaccessible due to reason', + }; + } + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + }; + }) + ); + + const { applications$ } = await service.start(startDeps); + const applications = await applications$.pipe(take(1)).toPromise(); + + expect(applications.size).toEqual(2); + expect(applications.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + navLinkStatus: AppNavLinkStatus.disabled, + status: AppStatus.inaccessible, + tooltip: 'App inaccessible due to reason', + }) + ); + expect(applications.get('app2')).toEqual( + expect.objectContaining({ + id: 'app2', + legacy: false, + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); + }); + + it('applies the most restrictive status in case of multiple updaters', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + setup.register(pluginId, createApp({ id: 'app1' })); + setup.registerAppUpdater( + new BehaviorSubject(app => { + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.disabled, + }; + }) + ); + setup.registerAppUpdater( + new BehaviorSubject(app => { + return { + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + }; + }) + ); + + const start = await service.start(startDeps); + const applications = await start.applications$.pipe(take(1)).toPromise(); + + expect(applications.size).toEqual(1); + expect(applications.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + navLinkStatus: AppNavLinkStatus.disabled, + status: AppStatus.inaccessible, + }) + ); + }); + + it('emits on applications$ when a status updater changes', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + setup.register(pluginId, createApp({ id: 'app1' })); + + const statusUpdater = new BehaviorSubject(app => { + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.disabled, + }; + }); + setup.registerAppUpdater(statusUpdater); + + const start = await service.start(startDeps); + let latestValue: ReadonlyMap = new Map(); + start.applications$.subscribe(apps => { + latestValue = apps; + }); + + expect(latestValue.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.disabled, + }) + ); + + statusUpdater.next(app => { + return { + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.hidden, + }; + }); + + expect(latestValue.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: false, + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); + }); + + it('also updates legacy apps', async () => { + const setup = service.setup(setupDeps); + + setup.registerLegacyApp(createLegacyApp({ id: 'app1' })); + + setup.registerAppUpdater( + new BehaviorSubject(app => { + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + tooltip: 'App inaccessible due to reason', + }; + }) + ); + + const start = await service.start(startDeps); + const applications = await start.applications$.pipe(take(1)).toPromise(); + + expect(applications.size).toEqual(1); + expect(applications.get('app1')).toEqual( + expect.objectContaining({ + id: 'app1', + legacy: true, + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + tooltip: 'App inaccessible due to reason', + }) + ); }); }); @@ -140,18 +420,16 @@ describe('#setup()', () => { const container = setupDeps.context.createContextContainer.mock.results[0].value; const pluginId = Symbol(); - registerMountContext(pluginId, 'test' as any, mount as any); + const mount = () => () => undefined; + registerMountContext(pluginId, 'test' as any, mount); expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', mount); }); }); describe('#start()', () => { - let setupDeps: MockLifecycle<'setup'>; - let startDeps: MockLifecycle<'start'>; - let service: ApplicationService; - beforeEach(() => { MockHistory.push.mockReset(); + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); setupDeps = { http, @@ -159,7 +437,7 @@ describe('#start()', () => { injectedMetadata: injectedMetadataServiceMock.createSetupContract(), }; setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); - startDeps = { http, injectedMetadata: setupDeps.injectedMetadata }; + startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); }); @@ -173,35 +451,40 @@ describe('#start()', () => { setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); const { register, registerLegacyApp } = service.setup(setupDeps); - register(Symbol(), { id: 'app1', mount } as any); - registerLegacyApp({ id: 'app2' } as any); - - const { availableApps, availableLegacyApps } = await service.start(startDeps); - - expect(availableApps).toMatchInlineSnapshot(` - Map { - "app1" => Object { - "appRoute": "/app/app1", - "id": "app1", - "mount": [Function], - }, - } - `); - expect(availableLegacyApps).toMatchInlineSnapshot(` - Map { - "app2" => Object { - "id": "app2", - }, - } - `); + register(Symbol(), createApp({ id: 'app1' })); + registerLegacyApp(createLegacyApp({ id: 'app2' })); + + const { applications$ } = await service.start(startDeps); + const availableApps = await applications$.pipe(take(1)).toPromise(); + + expect(availableApps.size).toEqual(2); + expect([...availableApps.keys()]).toEqual(['app1', 'app2']); + expect(availableApps.get('app1')).toEqual( + expect.objectContaining({ + appRoute: '/app/app1', + id: 'app1', + legacy: false, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }) + ); + expect(availableApps.get('app2')).toEqual( + expect.objectContaining({ + appUrl: '/my-url', + id: 'app2', + legacy: true, + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }) + ); }); it('passes appIds to capabilities', async () => { const { register } = service.setup(setupDeps); - register(Symbol(), { id: 'app1', mount } as any); - register(Symbol(), { id: 'app2', mount } as any); - register(Symbol(), { id: 'app3', mount } as any); + register(Symbol(), createApp({ id: 'app1' })); + register(Symbol(), createApp({ id: 'app2' })); + register(Symbol(), createApp({ id: 'app3' })); await service.start(startDeps); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ @@ -224,29 +507,15 @@ describe('#start()', () => { const { register, registerLegacyApp } = service.setup(setupDeps); - register(Symbol(), { id: 'app1', mount } as any); - registerLegacyApp({ id: 'legacyApp1' } as any); - register(Symbol(), { id: 'app2', mount } as any); - registerLegacyApp({ id: 'legacyApp2' } as any); + register(Symbol(), createApp({ id: 'app1' })); + registerLegacyApp(createLegacyApp({ id: 'legacyApp1' })); + register(Symbol(), createApp({ id: 'app2' })); + registerLegacyApp(createLegacyApp({ id: 'legacyApp2' })); - const { availableApps, availableLegacyApps } = await service.start(startDeps); + const { applications$ } = await service.start(startDeps); + const availableApps = await applications$.pipe(take(1)).toPromise(); - expect(availableApps).toMatchInlineSnapshot(` - Map { - "app1" => Object { - "appRoute": "/app/app1", - "id": "app1", - "mount": [Function], - }, - } - `); - expect(availableLegacyApps).toMatchInlineSnapshot(` - Map { - "legacyApp1" => Object { - "id": "legacyApp1", - }, - } - `); + expect([...availableApps.keys()]).toEqual(['app1', 'legacyApp1']); }); describe('getComponent', () => { @@ -264,6 +533,7 @@ describe('#start()', () => { } } mounters={Map {}} + setAppLeaveHandler={[Function]} /> `); }); @@ -291,9 +561,9 @@ describe('#start()', () => { it('creates URL for registered appId', async () => { const { register, registerLegacyApp } = service.setup(setupDeps); - register(Symbol(), { id: 'app1', mount } as any); - registerLegacyApp({ id: 'legacyApp1' } as any); - register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any); + register(Symbol(), createApp({ id: 'app1' })); + registerLegacyApp(createLegacyApp({ id: 'legacyApp1' })); + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' })); const { getUrlForApp } = await service.start(startDeps); @@ -320,41 +590,41 @@ describe('#start()', () => { const { navigateToApp } = await service.start(startDeps); - navigateToApp('myTestApp'); + await navigateToApp('myTestApp'); expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined); - navigateToApp('myOtherApp'); + await navigateToApp('myOtherApp'); expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined); }); it('changes the browser history for custom appRoutes', async () => { const { register } = service.setup(setupDeps); - register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any); + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' })); const { navigateToApp } = await service.start(startDeps); - navigateToApp('myTestApp'); + await navigateToApp('myTestApp'); expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined); - navigateToApp('app2'); + await navigateToApp('app2'); expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', undefined); }); it('appends a path if specified', async () => { const { register } = service.setup(setupDeps); - register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any); + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' })); const { navigateToApp } = await service.start(startDeps); - navigateToApp('myTestApp', { path: 'deep/link/to/location/2' }); + await navigateToApp('myTestApp', { path: 'deep/link/to/location/2' }); expect(MockHistory.push).toHaveBeenCalledWith( '/app/myTestApp/deep/link/to/location/2', undefined ); - navigateToApp('app2', { path: 'deep/link/to/location/2' }); + await navigateToApp('app2', { path: 'deep/link/to/location/2' }); expect(MockHistory.push).toHaveBeenCalledWith( '/custom/path/deep/link/to/location/2', undefined @@ -364,14 +634,14 @@ describe('#start()', () => { it('includes state if specified', async () => { const { register } = service.setup(setupDeps); - register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any); + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' })); const { navigateToApp } = await service.start(startDeps); - navigateToApp('myTestApp', { state: 'my-state' }); + await navigateToApp('myTestApp', { state: 'my-state' }); expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', 'my-state'); - navigateToApp('app2', { state: 'my-state' }); + await navigateToApp('app2', { state: 'my-state' }); expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', 'my-state'); }); @@ -382,7 +652,7 @@ describe('#start()', () => { const { navigateToApp } = await service.start(startDeps); - navigateToApp('myTestApp'); + await navigateToApp('myTestApp'); expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/myTestApp'); }); @@ -430,7 +700,7 @@ describe('#start()', () => { const { registerLegacyApp } = service.setup(setupDeps); - registerLegacyApp({ id: 'baseApp:legacyApp1' } as any); + registerLegacyApp(createLegacyApp({ id: 'baseApp:legacyApp1' })); const { navigateToApp } = await service.start(startDeps); @@ -439,3 +709,39 @@ describe('#start()', () => { }); }); }); + +describe('#stop()', () => { + let addListenerSpy: jest.SpyInstance; + let removeListenerSpy: jest.SpyInstance; + + beforeEach(() => { + addListenerSpy = jest.spyOn(window, 'addEventListener'); + removeListenerSpy = jest.spyOn(window, 'removeEventListener'); + + MockHistory.push.mockReset(); + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + setupDeps = { + http, + context: contextServiceMock.createSetupContract(), + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), + }; + setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); + startDeps = { http, overlays: overlayServiceMock.createStartContract() }; + service = new ApplicationService(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('removes the beforeunload listener', async () => { + service.setup(setupDeps); + await service.start(startDeps); + expect(addListenerSpy).toHaveBeenCalledTimes(1); + expect(addListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function)); + const handler = addListenerSpy.mock.calls[0][1]; + service.stop(); + expect(removeListenerSpy).toHaveBeenCalledTimes(1); + expect(removeListenerSpy).toHaveBeenCalledWith('beforeunload', handler); + }); +}); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index a96b9dea9b9c7..c69b96274aa95 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -18,31 +18,40 @@ */ import React from 'react'; -import { BehaviorSubject, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; -import { InjectedMetadataSetup, InjectedMetadataStart } from '../injected_metadata'; +import { InjectedMetadataSetup } from '../injected_metadata'; import { HttpSetup, HttpStart } from '../http'; +import { OverlayStart } from '../overlays'; import { ContextSetup, IContextContainer } from '../context'; import { AppRouter } from './ui'; -import { CapabilitiesService, Capabilities } from './capabilities'; +import { Capabilities, CapabilitiesService } from './capabilities'; import { App, - LegacyApp, + AppBase, + AppLeaveHandler, AppMount, AppMountDeprecated, AppMounter, - LegacyAppMounter, - Mounter, + AppNavLinkStatus, + AppStatus, + AppUpdatableFields, + AppUpdater, InternalApplicationSetup, InternalApplicationStart, + LegacyApp, + LegacyAppMounter, + Mounter, } from './types'; +import { getLeaveAction, isConfirmAction } from './application_leave'; interface SetupDeps { context: ContextSetup; http: HttpSetup; injectedMetadata: InjectedMetadataSetup; + history?: History; /** * Only necessary for redirecting to legacy apps * @deprecated @@ -51,19 +60,20 @@ interface SetupDeps { } interface StartDeps { - injectedMetadata: InjectedMetadataStart; http: HttpStart; + overlays: OverlayStart; } // Mount functions with two arguments are assumed to expect deprecated `context` object. const isAppMountDeprecated = (mount: (...args: any[]) => any): mount is AppMountDeprecated => mount.length === 2; -const filterAvailable = (map: Map, capabilities: Capabilities) => - new Map( - [...map].filter( +function filterAvailable(m: Map, capabilities: Capabilities) { + return new Map( + [...m].filter( ([id]) => capabilities.navLinks[id] === undefined || capabilities.navLinks[id] === true ) ); +} const findMounter = (mounters: Map, appRoute?: string) => [...mounters].find(([, mounter]) => mounter.appRoute === appRoute); const getAppUrl = (mounters: Map, appId: string, path: string = '') => @@ -71,16 +81,25 @@ const getAppUrl = (mounters: Map, appId: string, path: string = .replace(/\/{2,}/g, '/') // Remove duplicate slashes .replace(/\/$/, ''); // Remove trailing slash +const allApplicationsFilter = '__ALL__'; + +interface AppUpdaterWrapper { + application: string; + updater: AppUpdater; +} + /** * Service that is responsible for registering new applications. * @internal */ export class ApplicationService { - private readonly apps = new Map(); - private readonly legacyApps = new Map(); + private readonly apps = new Map(); private readonly mounters = new Map(); private readonly capabilities = new CapabilitiesService(); + private readonly appLeaveHandlers = new Map(); private currentAppId$ = new BehaviorSubject(undefined); + private readonly statusUpdaters$ = new BehaviorSubject>(new Map()); + private readonly subscriptions: Subscription[] = []; private stop$ = new Subject(); private registrationClosed = false; private history?: History; @@ -92,19 +111,34 @@ export class ApplicationService { http: { basePath }, injectedMetadata, redirectTo = (path: string) => (window.location.href = path), + history, }: SetupDeps): InternalApplicationSetup { const basename = basePath.get(); // Only setup history if we're not in legacy mode if (!injectedMetadata.getLegacyMode()) { - this.history = createBrowserHistory({ basename }); + this.history = history || createBrowserHistory({ basename }); } // If we do not have history available, use redirectTo to do a full page refresh. this.navigate = (url, state) => // basePath not needed here because `history` is configured with basename this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url)); + this.mountContext = context.createContextContainer(); + const registerStatusUpdater = (application: string, updater$: Observable) => { + const updaterId = Symbol(); + const subscription = updater$.subscribe(updater => { + const nextValue = new Map(this.statusUpdaters$.getValue()); + nextValue.set(updaterId, { + application, + updater, + }); + this.statusUpdaters$.next(nextValue); + }); + this.subscriptions.push(subscription); + }; + return { registerMountContext: this.mountContext!.registerContext, register: (plugin, app) => { @@ -139,7 +173,17 @@ export class ApplicationService { this.currentAppId$.next(app.id); return unmount; }; - this.apps.set(app.id, app); + + const { updater$, ...appProps } = app; + this.apps.set(app.id, { + ...appProps, + status: app.status ?? AppStatus.accessible, + navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default, + legacy: false, + }); + if (updater$) { + registerStatusUpdater(app.id, updater$); + } this.mounters.set(app.id, { appRoute: app.appRoute!, appBasePath: basePath.prepend(app.appRoute!), @@ -152,15 +196,25 @@ export class ApplicationService { if (this.registrationClosed) { throw new Error('Applications cannot be registered after "setup"'); - } else if (this.legacyApps.has(app.id)) { - throw new Error(`A legacy application is already registered with the id "${app.id}"`); + } else if (this.apps.has(app.id)) { + throw new Error(`An application is already registered with the id "${app.id}"`); } else if (basename && appRoute!.startsWith(basename)) { throw new Error('Cannot register an application route that includes HTTP base path'); } const appBasePath = basePath.prepend(appRoute); const mount: LegacyAppMounter = () => redirectTo(appBasePath); - this.legacyApps.set(app.id, app); + + const { updater$, ...appProps } = app; + this.apps.set(app.id, { + ...appProps, + status: app.status ?? AppStatus.accessible, + navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default, + legacy: true, + }); + if (updater$) { + registerStatusUpdater(app.id, updater$); + } this.mounters.set(app.id, { appRoute, appBasePath, @@ -168,40 +222,138 @@ export class ApplicationService { unmountBeforeMounting: true, }); }, + registerAppUpdater: (appUpdater$: Observable) => + registerStatusUpdater(allApplicationsFilter, appUpdater$), }; } - public async start({ injectedMetadata, http }: StartDeps): Promise { + public async start({ http, overlays }: StartDeps): Promise { if (!this.mountContext) { throw new Error('ApplicationService#setup() must be invoked before start.'); } this.registrationClosed = true; + window.addEventListener('beforeunload', this.onBeforeUnload); + const { capabilities } = await this.capabilities.start({ appIds: [...this.mounters.keys()], http, }); const availableMounters = filterAvailable(this.mounters, capabilities); + const availableApps = filterAvailable(this.apps, capabilities); + + const applications$ = new BehaviorSubject(availableApps); + this.statusUpdaters$ + .pipe( + map(statusUpdaters => { + return new Map( + [...availableApps].map(([id, app]) => [ + id, + updateStatus(app, [...statusUpdaters.values()]), + ]) + ); + }) + ) + .subscribe(apps => applications$.next(apps)); return { - availableApps: filterAvailable(this.apps, capabilities), - availableLegacyApps: filterAvailable(this.legacyApps, capabilities), + applications$, capabilities, currentAppId$: this.currentAppId$.pipe(takeUntil(this.stop$)), registerMountContext: this.mountContext.registerContext, getUrlForApp: (appId, { path }: { path?: string } = {}) => getAppUrl(availableMounters, appId, path), - navigateToApp: (appId, { path, state }: { path?: string; state?: any } = {}) => { - this.navigate!(getAppUrl(availableMounters, appId, path), state); - this.currentAppId$.next(appId); + navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { + const app = applications$.value.get(appId); + if (app && app.status !== AppStatus.accessible) { + // should probably redirect to the error page instead + throw new Error(`Trying to navigate to an inaccessible application: ${appId}`); + } + if (await this.shouldNavigate(overlays)) { + this.appLeaveHandlers.delete(this.currentAppId$.value!); + this.navigate!(getAppUrl(availableMounters, appId, path), state); + this.currentAppId$.next(appId); + } + }, + getComponent: () => { + if (!this.history) { + return null; + } + return ( + + ); }, - getComponent: () => - this.history ? : null, }; } + private setAppLeaveHandler = (appId: string, handler: AppLeaveHandler) => { + this.appLeaveHandlers.set(appId, handler); + }; + + private async shouldNavigate(overlays: OverlayStart): Promise { + const currentAppId = this.currentAppId$.value; + if (currentAppId === undefined) { + return true; + } + const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId)); + if (isConfirmAction(action)) { + const confirmed = await overlays.openConfirm(action.text, { + title: action.title, + 'data-test-subj': 'appLeaveConfirmModal', + }); + if (!confirmed) { + return false; + } + } + return true; + } + + private onBeforeUnload = (event: Event) => { + const currentAppId = this.currentAppId$.value; + if (currentAppId === undefined) { + return; + } + const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId)); + if (isConfirmAction(action)) { + event.preventDefault(); + // some browsers accept a string return value being the message displayed + event.returnValue = action.text as any; + } + }; + public stop() { this.stop$.next(); this.currentAppId$.complete(); + this.statusUpdaters$.complete(); + this.subscriptions.forEach(sub => sub.unsubscribe()); + window.removeEventListener('beforeunload', this.onBeforeUnload); } } + +const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapper[]): T => { + let changes: Partial = {}; + statusUpdaters.forEach(wrapper => { + if (wrapper.application !== allApplicationsFilter && wrapper.application !== app.id) { + return; + } + const fields = wrapper.updater(app); + if (fields) { + changes = { + ...changes, + ...fields, + // status and navLinkStatus enums are ordered by reversed priority + // if multiple updaters wants to change these fields, we will always follow the priority order. + status: Math.max(changes.status ?? 0, fields.status ?? 0), + navLinkStatus: Math.max(changes.navLinkStatus ?? 0, fields.navLinkStatus ?? 0), + }; + } + }); + return { + ...app, + ...changes, + }; +}; diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 9c4427c772a5e..e7ea330657648 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -27,8 +27,17 @@ export { AppUnmount, AppMountContext, AppMountParameters, + AppStatus, + AppNavLinkStatus, + AppUpdatableFields, + AppUpdater, ApplicationSetup, ApplicationStart, + AppLeaveHandler, + AppLeaveActionType, + AppLeaveAction, + AppLeaveDefaultAction, + AppLeaveConfirmAction, // Internal types InternalApplicationStart, LegacyApp, diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx new file mode 100644 index 0000000000000..edf3583f384b8 --- /dev/null +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { createRenderer } from './utils'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { ApplicationService } from '../application_service'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { contextServiceMock } from '../../context/context_service.mock'; +import { injectedMetadataServiceMock } from '../../injected_metadata/injected_metadata_service.mock'; +import { MockLifecycle } from '../test_types'; +import { overlayServiceMock } from '../../overlays/overlay_service.mock'; +import { AppMountParameters } from '../types'; + +describe('ApplicationService', () => { + let setupDeps: MockLifecycle<'setup'>; + let startDeps: MockLifecycle<'start'>; + let service: ApplicationService; + let history: MemoryHistory; + let update: ReturnType; + + const navigate = (path: string) => { + history.push(path); + return update(); + }; + + beforeEach(() => { + history = createMemoryHistory(); + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + + http.post.mockResolvedValue({ navLinks: {} }); + + setupDeps = { + http, + context: contextServiceMock.createSetupContract(), + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), + history: history as any, + }; + setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); + startDeps = { http, overlays: overlayServiceMock.createStartContract() }; + service = new ApplicationService(); + }); + + describe('leaving an application that registered an app leave handler', () => { + it('navigates to the new app if action is default', async () => { + startDeps.overlays.openConfirm.mockResolvedValue(true); + + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: ({ onAppLeave }: AppMountParameters) => { + onAppLeave(actions => actions.default()); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: ({}: AppMountParameters) => { + return () => undefined; + }, + }); + + const { navigateToApp, getComponent } = await service.start(startDeps); + + update = createRenderer(getComponent()); + + await navigate('/app/app1'); + await navigateToApp('app2'); + + expect(startDeps.overlays.openConfirm).not.toHaveBeenCalled(); + expect(history.entries.length).toEqual(3); + expect(history.entries[2].pathname).toEqual('/app/app2'); + }); + + it('navigates to the new app if action is confirm and user accepted', async () => { + startDeps.overlays.openConfirm.mockResolvedValue(true); + + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: ({ onAppLeave }: AppMountParameters) => { + onAppLeave(actions => actions.confirm('confirmation-message', 'confirmation-title')); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: ({}: AppMountParameters) => { + return () => undefined; + }, + }); + + const { navigateToApp, getComponent } = await service.start(startDeps); + + update = createRenderer(getComponent()); + + await navigate('/app/app1'); + await navigateToApp('app2'); + + expect(startDeps.overlays.openConfirm).toHaveBeenCalledTimes(1); + expect(startDeps.overlays.openConfirm).toHaveBeenCalledWith( + 'confirmation-message', + expect.objectContaining({ title: 'confirmation-title' }) + ); + expect(history.entries.length).toEqual(3); + expect(history.entries[2].pathname).toEqual('/app/app2'); + }); + + it('blocks navigation to the new app if action is confirm and user declined', async () => { + startDeps.overlays.openConfirm.mockResolvedValue(false); + + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: ({ onAppLeave }: AppMountParameters) => { + onAppLeave(actions => actions.confirm('confirmation-message', 'confirmation-title')); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: ({}: AppMountParameters) => { + return () => undefined; + }, + }); + + const { navigateToApp, getComponent } = await service.start(startDeps); + + update = createRenderer(getComponent()); + + await navigate('/app/app1'); + await navigateToApp('app2'); + + expect(startDeps.overlays.openConfirm).toHaveBeenCalledTimes(1); + expect(startDeps.overlays.openConfirm).toHaveBeenCalledWith( + 'confirmation-message', + expect.objectContaining({ title: 'confirmation-title' }) + ); + expect(history.entries.length).toEqual(2); + expect(history.entries[1].pathname).toEqual('/app/app1'); + }); + }); +}); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 10544c348afb0..cc71cf8722df4 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -36,6 +36,7 @@ describe('AppContainer', () => { const mockMountersToMounters = () => new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter])); + const setAppLeaveHandlerMock = () => undefined; beforeEach(() => { mounters = new Map([ @@ -46,7 +47,13 @@ describe('AppContainer', () => { createAppMounter('app3', '
App 3
', '/custom/path'), ] as Array>); history = createMemoryHistory(); - update = createRenderer(); + update = createRenderer( + + ); }); it('calls mount handler and returned unmount function when navigating between apps', async () => { @@ -78,7 +85,13 @@ describe('AppContainer', () => { mounters.set(...createAppMounter('spaces', '
Custom Space
', '/spaces/fake-login')); mounters.set(...createAppMounter('login', '
Login Page
', '/fake-login')); history = createMemoryHistory(); - update = createRenderer(); + update = createRenderer( + + ); await navigate('/fake-login'); @@ -90,7 +103,13 @@ describe('AppContainer', () => { mounters.set(...createAppMounter('login', '
Login Page
', '/fake-login')); mounters.set(...createAppMounter('spaces', '
Custom Space
', '/spaces/fake-login')); history = createMemoryHistory(); - update = createRenderer(); + update = createRenderer( + + ); await navigate('/spaces/fake-login'); @@ -124,7 +143,13 @@ describe('AppContainer', () => { it('should not remount when when changing pages within app using hash history', async () => { history = createHashHistory(); - update = createRenderer(); + update = createRenderer( + + ); const { mounter, unmount } = mounters.get('app1')!; await navigate('/app/app1/page1'); @@ -153,6 +178,7 @@ describe('AppContainer', () => { Object { "appBasePath": "/app/legacyApp1", "element":
, + "onAppLeave": [Function], }, ] `); @@ -165,6 +191,7 @@ describe('AppContainer', () => { Object { "appBasePath": "/app/baseApp", "element":
, + "onAppLeave": [Function], }, ] `); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index c026851af7eb8..0d955482d2226 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -34,6 +34,9 @@ import { SavedObjectsStart } from '../saved_objects'; /** @public */ export interface AppBase { + /** + * The unique identifier of the application + */ id: string; /** @@ -41,15 +44,62 @@ export interface AppBase { */ title: string; + /** + * The initial status of the application. + * Defaulting to `accessible` + */ + status?: AppStatus; + + /** + * The initial status of the application's navLink. + * Defaulting to `visible` if `status` is `accessible` and `hidden` if status is `inaccessible` + * See {@link AppNavLinkStatus} + */ + navLinkStatus?: AppNavLinkStatus; + + /** + * An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime. + * + * @example + * + * How to update an application navLink at runtime + * + * ```ts + * // inside your plugin's setup function + * export class MyPlugin implements Plugin { + * private appUpdater = new BehaviorSubject(() => ({})); + * + * setup({ application }) { + * application.register({ + * id: 'my-app', + * title: 'My App', + * updater$: this.appUpdater, + * async mount(params) { + * const { renderApp } = await import('./application'); + * return renderApp(params); + * }, + * }); + * } + * + * start() { + * // later, when the navlink needs to be updated + * appUpdater.next(() => { + * navLinkStatus: AppNavLinkStatus.disabled, + * }) + * } + * ``` + */ + updater$?: Observable; + /** * An ordinal used to sort nav links relative to one another for display. */ order?: number; /** - * An observable for a tooltip shown when hovering over app link. + * A tooltip shown when hovering over app link. */ - tooltip$?: Observable; + tooltip?: string; /** * A EUI iconType that will be used for the app's icon. This icon @@ -67,8 +117,76 @@ export interface AppBase { * Custom capabilities defined by the app. */ capabilities?: Partial; + + /** + * Flag to keep track of legacy applications. + * For internal use only. any value will be overridden when registering an App. + * + * @internal + */ + legacy?: boolean; + + /** + * Hide the UI chrome when the application is mounted. Defaults to `false`. + * Takes precedence over chrome service visibility settings. + */ + chromeless?: boolean; } +/** + * Accessibility status of an application. + * + * @public + */ +export enum AppStatus { + /** + * Application is accessible. + */ + accessible = 0, + /** + * Application is not accessible. + */ + inaccessible = 1, +} + +/** + * Status of the application's navLink. + * + * @public + */ +export enum AppNavLinkStatus { + /** + * The application navLink will be `visible` if the application's {@link AppStatus} is set to `accessible` + * and `hidden` if the application status is set to `inaccessible`. + */ + default = 0, + /** + * The application navLink is visible and clickable in the navigation bar. + */ + visible = 1, + /** + * The application navLink is visible but inactive and not clickable in the navigation bar. + */ + disabled = 2, + /** + * The application navLink does not appear in the navigation bar. + */ + hidden = 3, +} + +/** + * Defines the list of fields that can be updated via an {@link AppUpdater}. + * @public + */ +export type AppUpdatableFields = Pick; + +/** + * Updater for applications. + * see {@link ApplicationSetup} + * @public + */ +export type AppUpdater = (app: AppBase) => Partial | undefined; + /** * Extension of {@link AppBase | common app properties} with the mount function. * @public @@ -230,6 +348,117 @@ export interface AppMountParameters { * ``` */ appBasePath: string; + + /** + * A function that can be used to register a handler that will be called + * when the user is leaving the current application, allowing to + * prompt a confirmation message before actually changing the page. + * + * This will be called either when the user goes to another application, or when + * trying to close the tab or manually changing the url. + * + * @example + * + * ```ts + * // application.tsx + * import React from 'react'; + * import ReactDOM from 'react-dom'; + * import { BrowserRouter, Route } from 'react-router-dom'; + * + * import { CoreStart, AppMountParams } from 'src/core/public'; + * import { MyPluginDepsStart } from './plugin'; + * + * export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { + * const { renderApp, hasUnsavedChanges } = await import('./application'); + * onAppLeave(actions => { + * if(hasUnsavedChanges()) { + * return actions.confirm('Some changes were not saved. Are you sure you want to leave?'); + * } + * return actions.default(); + * }); + * return renderApp(params); + * } + * ``` + */ + onAppLeave: (handler: AppLeaveHandler) => void; +} + +/** + * A handler that will be executed before leaving the application, either when + * going to another application or when closing the browser tab or manually changing + * the url. + * Should return `confirm` to to prompt a message to the user before leaving the page, or `default` + * to keep the default behavior (doing nothing). + * + * See {@link AppMountParameters} for detailed usage examples. + * + * @public + */ +export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction; + +/** + * Possible type of actions on application leave. + * + * @public + */ +export enum AppLeaveActionType { + confirm = 'confirm', + default = 'default', +} + +/** + * Action to return from a {@link AppLeaveHandler} to execute the default + * behaviour when leaving the application. + * + * See {@link AppLeaveActionFactory} + * + * @public + */ +export interface AppLeaveDefaultAction { + type: AppLeaveActionType.default; +} + +/** + * Action to return from a {@link AppLeaveHandler} to show a confirmation + * message when trying to leave an application. + * + * See {@link AppLeaveActionFactory} + * + * @public + */ +export interface AppLeaveConfirmAction { + type: AppLeaveActionType.confirm; + text: string; + title?: string; +} + +/** + * Possible actions to return from a {@link AppLeaveHandler} + * + * See {@link AppLeaveConfirmAction} and {@link AppLeaveDefaultAction} + * + * @public + * */ +export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction; + +/** + * Factory provided when invoking a {@link AppLeaveHandler} to retrieve the {@link AppLeaveAction} to execute. + */ +export interface AppLeaveActionFactory { + /** + * Returns a confirm action, resulting on prompting a message to the user before leaving the + * application, allowing him to choose if he wants to stay on the app or confirm that he + * wants to leave. + * + * @param text The text to display in the confirmation message + * @param title (optional) title to display in the confirmation message + */ + confirm(text: string, title?: string): AppLeaveConfirmAction; + /** + * Returns a default action, resulting on executing the default behavior when + * the user tries to leave an application + */ + default(): AppLeaveDefaultAction; } /** @@ -263,6 +492,35 @@ export interface ApplicationSetup { */ register(app: App): void; + /** + * Register an application updater that can be used to change the {@link AppUpdatableFields} fields + * of all applications at runtime. + * + * This is meant to be used by plugins that needs to updates the whole list of applications. + * To only updates a specific application, use the `updater$` property of the registered application instead. + * + * @example + * + * How to register an application updater that disables some applications: + * + * ```ts + * // inside your plugin's setup function + * export class MyPlugin implements Plugin { + * setup({ application }) { + * application.registerAppUpdater( + * new BehaviorSubject(app => { + * if (myPluginApi.shouldDisable(app)) + * return { + * status: AppStatus.inaccessible, + * }; + * }) + * ); + * } + * } + * ``` + */ + registerAppUpdater(appUpdater$: Observable): void; + /** * Register a context provider for application mounting. Will only be available to applications that depend on the * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. @@ -278,7 +536,7 @@ export interface ApplicationSetup { } /** @internal */ -export interface InternalApplicationSetup { +export interface InternalApplicationSetup extends Pick { /** * Register an mountable application to the system. * @param plugin - opaque ID of the plugin that registers this application @@ -317,13 +575,13 @@ export interface ApplicationStart { capabilities: RecursiveReadonly; /** - * Navigiate to a given app + * Navigate to a given app * * @param appId * @param options.path - optional path inside application to deep link to * @param options.state - optional state to forward to the application */ - navigateToApp(appId: string, options?: { path?: string; state?: any }): void; + navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; /** * Returns a relative URL to a given app, including the global base path. @@ -351,16 +609,11 @@ export interface ApplicationStart { export interface InternalApplicationStart extends Pick { /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - */ - availableApps: ReadonlyMap; - /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - * @internal + * Apps available based on the current capabilities. + * Should be used to show navigation links and make routing decisions. + * Applications manually disabled from the client-side using {@link AppUpdater} */ - availableLegacyApps: ReadonlyMap; + applications$: Observable>; /** * Register a context provider for application mounting. Will only be available to applications that depend on the diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 153582e805fa1..8afd4d0ca0551 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -26,15 +26,20 @@ import React, { MutableRefObject, } from 'react'; -import { AppUnmount, Mounter } from '../types'; +import { AppUnmount, Mounter, AppLeaveHandler } from '../types'; import { AppNotFound } from './app_not_found_screen'; interface Props { appId: string; mounter?: Mounter; + setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; } -export const AppContainer: FunctionComponent = ({ mounter, appId }: Props) => { +export const AppContainer: FunctionComponent = ({ + mounter, + appId, + setAppLeaveHandler, +}: Props) => { const [appNotFound, setAppNotFound] = useState(false); const elementRef = useRef(null); const unmountRef: MutableRefObject = useRef(null); @@ -59,13 +64,14 @@ export const AppContainer: FunctionComponent = ({ mounter, appId }: Props (await mounter.mount({ appBasePath: mounter.appBasePath, element: elementRef.current!, + onAppLeave: handler => setAppLeaveHandler(appId, handler), })) || null; setAppNotFound(false); }; mount(); return unmount; - }, [mounter]); + }, [appId, mounter, setAppLeaveHandler]); return ( diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 8db46f9794277..2ee90c3bf5e29 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -21,19 +21,20 @@ import React, { FunctionComponent } from 'react'; import { History } from 'history'; import { Router, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { Mounter } from '../types'; +import { Mounter, AppLeaveHandler } from '../types'; import { AppContainer } from './app_container'; interface Props { mounters: Map; history: History; + setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; } interface Params { appId: string; } -export const AppRouter: FunctionComponent = ({ history, mounters }) => ( +export const AppRouter: FunctionComponent = ({ history, mounters, setAppLeaveHandler }) => ( {[...mounters].flatMap(([appId, mounter]) => @@ -45,7 +46,13 @@ export const AppRouter: FunctionComponent = ({ history, mounters }) => ( } + render={() => ( + + )} />, ] )} @@ -61,7 +68,9 @@ export const AppRouter: FunctionComponent = ({ history, mounters }) => ( ? [appId, mounters.get(appId)] : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? []; - return ; + return ( + + ); }} /> diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index d9c35b20db03b..abd04722a49f2 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -18,7 +18,7 @@ */ import * as Rx from 'rxjs'; -import { toArray } from 'rxjs/operators'; +import { take, toArray } from 'rxjs/operators'; import { shallow } from 'enzyme'; import React from 'react'; @@ -54,7 +54,9 @@ function defaultStartDeps(availableApps?: App[]) { }; if (availableApps) { - deps.application.availableApps = new Map(availableApps.map(app => [app.id, app])); + deps.application.applications$ = new Rx.BehaviorSubject>( + new Map(availableApps.map(app => [app.id, app])) + ); } return deps; @@ -211,13 +213,14 @@ describe('start', () => { new FakeApp('beta', true), new FakeApp('gamma', false), ]); - const { availableApps, navigateToApp } = startDeps.application; + const { applications$, navigateToApp } = startDeps.application; const { chrome, service } = await start({ startDeps }); const promise = chrome .getIsVisible$() .pipe(toArray()) .toPromise(); + const availableApps = await applications$.pipe(take(1)).toPromise(); [...availableApps.keys()].forEach(appId => navigateToApp(appId)); service.stop(); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 18c0c9870d72f..a674b49a8e134 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { BehaviorSubject, Observable, ReplaySubject, combineLatest, of, merge } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; import { i18n } from '@kbn/i18n'; @@ -118,11 +118,12 @@ export class ChromeService { // combineLatest below regardless of having an application value yet. of(isEmbedded), application.currentAppId$.pipe( - map( - appId => - !!appId && - application.availableApps.has(appId) && - !!application.availableApps.get(appId)!.chromeless + flatMap(appId => + application.applications$.pipe( + map(applications => { + return !!appId && applications.has(appId) && !!applications.get(appId)!.chromeless; + }) + ) ) ) ); diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index d87d171e028e1..3b16c030ddcc9 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -63,7 +63,7 @@ export interface ChromeNavLink { /** LEGACY FIELDS */ /** - * A url base that legacy apps can set to match deep URLs to an applcation. + * A url base that legacy apps can set to match deep URLs to an application. * * @internalRemarks * This should be removed once legacy apps are gone. diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index 5a45491df28e7..3d9a4bfdb6a56 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -20,34 +20,47 @@ import { NavLinksService } from './nav_links_service'; import { take, map, takeLast } from 'rxjs/operators'; import { App, LegacyApp } from '../../application'; +import { BehaviorSubject } from 'rxjs'; -const mockAppService = { - availableApps: new Map( - ([ - { id: 'app1', order: 0, title: 'App 1', icon: 'app1' }, - { - id: 'app2', - order: -10, - title: 'App 2', - euiIconType: 'canvasApp', - }, - { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }, - ] as App[]).map(app => [app.id, app]) - ), - availableLegacyApps: new Map( - ([ - { id: 'legacyApp1', order: 5, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, - { - id: 'legacyApp2', - order: -5, - title: 'Legacy App 2', - euiIconType: 'canvasApp', - appUrl: '/app2', - }, - { id: 'legacyApp3', order: 15, title: 'Legacy App 3', appUrl: '/app3' }, - ] as LegacyApp[]).map(app => [app.id, app]) - ), -} as any; +const availableApps = new Map([ + ['app1', { id: 'app1', order: 0, title: 'App 1', icon: 'app1' }], + [ + 'app2', + { + id: 'app2', + order: -10, + title: 'App 2', + euiIconType: 'canvasApp', + }, + ], + ['chromelessApp', { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }], + [ + 'legacyApp1', + { + id: 'legacyApp1', + order: 5, + title: 'Legacy App 1', + icon: 'legacyApp1', + appUrl: '/app1', + legacy: true, + }, + ], + [ + 'legacyApp2', + { + id: 'legacyApp2', + order: -10, + title: 'Legacy App 2', + euiIconType: 'canvasApp', + appUrl: '/app2', + legacy: true, + }, + ], + [ + 'legacyApp3', + { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3', legacy: true }, + ], +]); const mockHttp = { basePath: { @@ -57,10 +70,16 @@ const mockHttp = { describe('NavLinksService', () => { let service: NavLinksService; + let mockAppService: any; let start: ReturnType; beforeEach(() => { service = new NavLinksService(); + mockAppService = { + applications$: new BehaviorSubject>( + availableApps as any + ), + }; start = service.start({ application: mockAppService, http: mockHttp }); }); @@ -183,22 +202,36 @@ describe('NavLinksService', () => { .toPromise() ).toEqual(['legacyApp1']); }); + + it('still removes all other links when availableApps are re-emitted', async () => { + start.showOnly('legacyApp2'); + mockAppService.applications$.next(mockAppService.applications$.value); + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.map(l => l.id)) + ) + .toPromise() + ).toEqual(['legacyApp2']); + }); }); describe('#update()', () => { it('updates the navlinks and returns the updated link', async () => { - expect(start.update('legacyApp1', { hidden: true })).toMatchInlineSnapshot(` - Object { - "appUrl": "/app1", - "baseUrl": "http://localhost/wow/app1", - "hidden": true, - "icon": "legacyApp1", - "id": "legacyApp1", - "legacy": true, - "order": 5, - "title": "Legacy App 1", - } - `); + expect(start.update('legacyApp1', { hidden: true })).toEqual( + expect.objectContaining({ + appUrl: '/app1', + disabled: false, + hidden: true, + icon: 'legacyApp1', + id: 'legacyApp1', + legacy: true, + order: 5, + title: 'Legacy App 1', + }) + ); const hiddenLinkIds = await start .getNavLinks$() .pipe( @@ -212,6 +245,19 @@ describe('NavLinksService', () => { it('returns undefined if link does not exist', () => { expect(start.update('fake', { hidden: true })).toBeUndefined(); }); + + it('keeps the updated link when availableApps are re-emitted', async () => { + start.update('legacyApp1', { hidden: true }); + mockAppService.applications$.next(mockAppService.applications$.value); + const hiddenLinkIds = await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.filter(l => l.hidden).map(l => l.id)) + ) + .toPromise(); + expect(hiddenLinkIds).toEqual(['legacyApp1']); + }); }); describe('#enableForcedAppSwitcherNavigation()', () => { diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 31a729f90cd93..650ef77b6fe42 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -18,11 +18,13 @@ */ import { sortBy } from 'lodash'; -import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs'; +import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; -import { NavLinkWrapper, ChromeNavLinkUpdateableFields, ChromeNavLink } from './nav_link'; + import { InternalApplicationStart } from '../../application'; import { HttpStart } from '../../http'; +import { ChromeNavLink, ChromeNavLinkUpdateableFields, NavLinkWrapper } from './nav_link'; +import { toNavLink } from './to_nav_link'; interface StartDeps { application: InternalApplicationStart; @@ -95,39 +97,38 @@ export interface ChromeNavLinks { getForceAppSwitcherNavigation$(): Observable; } +type LinksUpdater = (navLinks: Map) => Map; + export class NavLinksService { private readonly stop$ = new ReplaySubject(1); public start({ application, http }: StartDeps): ChromeNavLinks { - const appLinks = [...application.availableApps] - .filter(([, app]) => !app.chromeless) - .map( - ([appId, app]) => - [ - appId, - new NavLinkWrapper({ - ...app, - legacy: false, - baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)), - }), - ] as [string, NavLinkWrapper] - ); - - const legacyAppLinks = [...application.availableLegacyApps].map( - ([appId, app]) => - [ - appId, - new NavLinkWrapper({ - ...app, - legacy: true, - baseUrl: relativeToAbsolute(http.basePath.prepend(app.appUrl)), - }), - ] as [string, NavLinkWrapper] + const appLinks$ = application.applications$.pipe( + map(apps => { + return new Map( + [...apps] + .filter(([, app]) => !app.chromeless) + .map(([appId, app]) => [appId, toNavLink(app, http.basePath)]) + ); + }) ); - const navLinks$ = new BehaviorSubject>( - new Map([...legacyAppLinks, ...appLinks]) - ); + // now that availableApps$ is an observable, we need to keep record of all + // manual link modifications to be able to re-apply then after every + // availableApps$ changes. + const linkUpdaters$ = new BehaviorSubject([]); + const navLinks$ = new BehaviorSubject>(new Map()); + + combineLatest([appLinks$, linkUpdaters$]) + .pipe( + map(([appLinks, linkUpdaters]) => { + return linkUpdaters.reduce((links, updater) => updater(links), appLinks); + }) + ) + .subscribe(navlinks => { + navLinks$.next(navlinks); + }); + const forceAppSwitcherNavigation$ = new BehaviorSubject(false); return { @@ -153,7 +154,10 @@ export class NavLinksService { return; } - navLinks$.next(new Map([...navLinks$.value.entries()].filter(([linkId]) => linkId === id))); + const updater: LinksUpdater = navLinks => + new Map([...navLinks.entries()].filter(([linkId]) => linkId === id)); + + linkUpdaters$.next([...linkUpdaters$.value, updater]); }, update(id: string, values: ChromeNavLinkUpdateableFields) { @@ -161,17 +165,17 @@ export class NavLinksService { return; } - navLinks$.next( + const updater: LinksUpdater = navLinks => new Map( - [...navLinks$.value.entries()].map(([linkId, link]) => { + [...navLinks.entries()].map(([linkId, link]) => { return [linkId, link.id === id ? link.update(values) : link] as [ string, NavLinkWrapper ]; }) - ) - ); + ); + linkUpdaters$.next([...linkUpdaters$.value, updater]); return this.get(id); }, @@ -196,10 +200,3 @@ function sortNavLinks(navLinks: ReadonlyMap) { 'order' ); } - -function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts new file mode 100644 index 0000000000000..23fdabe0f3430 --- /dev/null +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -0,0 +1,178 @@ +/* + * 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 { App, AppMount, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; +import { toNavLink } from './to_nav_link'; + +import { httpServiceMock } from '../../mocks'; + +function mount() {} + +const app = (props: Partial = {}): App => ({ + mount: (mount as unknown) as AppMount, + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + appRoute: `/app/some-id`, + legacy: false, + ...props, +}); + +const legacyApp = (props: Partial = {}): LegacyApp => ({ + appUrl: '/my-app-url', + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + legacy: true, + ...props, +}); + +describe('toNavLink', () => { + const basePath = httpServiceMock.createSetupContract({ basePath: '/base-path' }).basePath; + + it('uses the application properties when creating the navLink', () => { + const link = toNavLink( + app({ + id: 'id', + title: 'title', + order: 12, + tooltip: 'tooltip', + euiIconType: 'my-icon', + }), + basePath + ); + expect(link.properties).toEqual( + expect.objectContaining({ + id: 'id', + title: 'title', + order: 12, + tooltip: 'tooltip', + euiIconType: 'my-icon', + }) + ); + }); + + it('flags legacy apps when converting to navLink', () => { + expect(toNavLink(app({}), basePath).properties.legacy).toEqual(false); + expect(toNavLink(legacyApp({}), basePath).properties.legacy).toEqual(true); + }); + + it('handles applications with custom app route', () => { + const link = toNavLink( + app({ + appRoute: '/my-route/my-path', + }), + basePath + ); + expect(link.properties.baseUrl).toEqual('http://localhost/base-path/my-route/my-path'); + }); + + it('uses appUrl when converting legacy applications', () => { + expect( + toNavLink( + legacyApp({ + appUrl: '/my-legacy-app/#foo', + }), + basePath + ).properties + ).toEqual( + expect.objectContaining({ + baseUrl: 'http://localhost/base-path/my-legacy-app/#foo', + }) + ); + }); + + it('uses the application status when the navLinkStatus is set to default', () => { + expect( + toNavLink( + app({ + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }), + basePath + ).properties + ).toEqual( + expect.objectContaining({ + disabled: false, + hidden: false, + }) + ); + + expect( + toNavLink( + app({ + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.inaccessible, + }), + basePath + ).properties + ).toEqual( + expect.objectContaining({ + disabled: false, + hidden: true, + }) + ); + }); + + it('uses the navLinkStatus of the application to set the hidden and disabled properties', () => { + expect( + toNavLink( + app({ + navLinkStatus: AppNavLinkStatus.visible, + }), + basePath + ).properties + ).toEqual( + expect.objectContaining({ + disabled: false, + hidden: false, + }) + ); + + expect( + toNavLink( + app({ + navLinkStatus: AppNavLinkStatus.hidden, + }), + basePath + ).properties + ).toEqual( + expect.objectContaining({ + disabled: false, + hidden: true, + }) + ); + + expect( + toNavLink( + app({ + navLinkStatus: AppNavLinkStatus.disabled, + }), + basePath + ).properties + ).toEqual( + expect.objectContaining({ + disabled: true, + hidden: false, + }) + ); + }); +}); diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts new file mode 100644 index 0000000000000..18e4b7b26b6ba --- /dev/null +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -0,0 +1,48 @@ +/* + * 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 { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; +import { IBasePath } from '../../http'; +import { NavLinkWrapper } from './nav_link'; + +export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { + const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; + return new NavLinkWrapper({ + ...app, + hidden: useAppStatus + ? app.status === AppStatus.inaccessible + : app.navLinkStatus === AppNavLinkStatus.hidden, + disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, + legacy: isLegacyApp(app), + baseUrl: isLegacyApp(app) + ? relativeToAbsolute(basePath.prepend(app.appUrl)) + : relativeToAbsolute(basePath.prepend(app.appRoute!)), + }); +} + +function relativeToAbsolute(url: string) { + // convert all link urls to absolute urls + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} + +function isLegacyApp(app: App | LegacyApp): app is LegacyApp { + return app.legacy === true; +} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 75f78ac8b2fa0..0447add491788 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -309,7 +309,7 @@ class HeaderUI extends Component { .filter(navLink => !navLink.hidden) .map(navLink => ({ key: navLink.id, - label: navLink.title, + label: navLink.tooltip ?? navLink.title, // Use href and onClick to support "open in new tab" and SPA navigation in the same link href: navLink.href, diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 485c11aae6508..5b31c740518e4 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -214,7 +214,6 @@ export class CoreSystem { const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); - const application = await this.application.start({ http, injectedMetadata }); await this.integrations.start({ uiSettings }); const coreUiTargetDomElement = document.createElement('div'); @@ -239,6 +238,7 @@ export class CoreSystem { overlays, targetDomElement: notificationsTargetDomElement, }); + const application = await this.application.start({ http, overlays }); const chrome = await this.chrome.start({ application, docLinks, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 7488f9b973b71..5b17eccc37f8b 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -89,6 +89,15 @@ export { AppUnmount, AppMountContext, AppMountParameters, + AppLeaveHandler, + AppLeaveActionType, + AppLeaveAction, + AppLeaveDefaultAction, + AppLeaveConfirmAction, + AppStatus, + AppNavLinkStatus, + AppUpdatableFields, + AppUpdater, } from './application'; export { diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index a4fdd86de5311..f906aff1759e2 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -81,6 +81,7 @@ export class LegacyPlatformService { ...core, getStartServices: () => this.startDependencies, application: { + ...core.application, register: notSupported(`core.application.register()`), registerMountContext: notSupported(`core.application.registerMountContext()`), }, diff --git a/src/core/public/notifications/toasts/global_toast_list.test.tsx b/src/core/public/notifications/toasts/global_toast_list.test.tsx index 61d73ac233188..dc2a9dabe791e 100644 --- a/src/core/public/notifications/toasts/global_toast_list.test.tsx +++ b/src/core/public/notifications/toasts/global_toast_list.test.tsx @@ -57,9 +57,9 @@ it('subscribes to toasts$ on mount and unsubscribes on unmount', () => { it('passes latest value from toasts$ to ', () => { const el = shallow( render({ - toasts$: Rx.from([[], [{ id: 1 }], [{ id: 1 }, { id: 2 }]]) as any, + toasts$: Rx.from([[], [{ id: '1' }], [{ id: '1' }, { id: '2' }]]) as any, }) ); - expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([{ id: 1 }, { id: 2 }]); + expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([{ id: '1' }, { id: '2' }]); }); diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index 131ec836f5252..7eaa1c3af2079 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -8,6 +8,129 @@ Array [ ] `; +exports[`ModalService openConfirm() renders a mountpoint confirm message 1`] = ` +Array [ + Array [ + + + + + + + , +
, + ], +] +`; + +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; + +exports[`ModalService openConfirm() renders a string confirm message 1`] = ` +Array [ + Array [ + + + + Some message + + + , +
, + ], +] +`; + +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; + +exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` +Array [ + Array [ + + + + confirm 1 + + + , +
, + ], + Array [ + + + + some confirm + + + , +
, + ], +] +`; + +exports[`ModalService openConfirm() with a currently active modal replaces the current modal with the new confirm 1`] = ` +Array [ + Array [ + + + + + + + , +
, + ], + Array [ + + + + some confirm + + + , +
, + ], +] +`; + exports[`ModalService openModal() renders a modal to the DOM 1`] = ` Array [ Array [ @@ -31,6 +154,43 @@ Array [ exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; +exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = ` +Array [ + Array [ + + + + confirm 1 + + + , +
, + ], + Array [ + + + + some confirm + + + , +
, + ], +] +`; + exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = ` Array [ Array [ diff --git a/src/core/public/overlays/modal/modal_service.mock.ts b/src/core/public/overlays/modal/modal_service.mock.ts index 726209b8f277c..5ac49874dcf93 100644 --- a/src/core/public/overlays/modal/modal_service.mock.ts +++ b/src/core/public/overlays/modal/modal_service.mock.ts @@ -25,6 +25,7 @@ const createStartContractMock = () => { close: jest.fn(), onClose: Promise.resolve(), }), + openConfirm: jest.fn().mockResolvedValue(true), }; return startContract; }; diff --git a/src/core/public/overlays/modal/modal_service.test.tsx b/src/core/public/overlays/modal/modal_service.test.tsx index 582c2697aef30..8b68075bb2a00 100644 --- a/src/core/public/overlays/modal/modal_service.test.tsx +++ b/src/core/public/overlays/modal/modal_service.test.tsx @@ -80,6 +80,91 @@ describe('ModalService', () => { expect(onCloseComplete).toBeCalledTimes(1); }); }); + + describe('with a currently active confirm', () => { + let confirm1: Promise; + + beforeEach(() => { + confirm1 = modals.openConfirm('confirm 1'); + }); + + it('replaces the current confirm with the new one', () => { + modals.openConfirm('some confirm'); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + }); + + it('resolves the previous confirm promise', async () => { + modals.open(mountReactNode(Flyout content 2)); + expect(await confirm1).toEqual(false); + }); + }); + }); + + describe('openConfirm()', () => { + it('renders a mountpoint confirm message', () => { + expect(mockReactDomRender).not.toHaveBeenCalled(); + modals.openConfirm(container => { + const content = document.createElement('span'); + content.textContent = 'Modal content'; + container.append(content); + return () => {}; + }); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + const modalContent = mount(mockReactDomRender.mock.calls[0][0]); + expect(modalContent.html()).toMatchSnapshot(); + }); + + it('renders a string confirm message', () => { + expect(mockReactDomRender).not.toHaveBeenCalled(); + modals.openConfirm('Some message'); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + const modalContent = mount(mockReactDomRender.mock.calls[0][0]); + expect(modalContent.html()).toMatchSnapshot(); + }); + + describe('with a currently active modal', () => { + let ref1: OverlayRef; + + beforeEach(() => { + ref1 = modals.open(mountReactNode(Modal content 1)); + }); + + it('replaces the current modal with the new confirm', () => { + modals.openConfirm('some confirm'); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + expect(() => ref1.close()).not.toThrowError(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + }); + + it('resolves onClose on the previous ref', async () => { + const onCloseComplete = jest.fn(); + ref1.onClose.then(onCloseComplete); + modals.openConfirm('some confirm'); + await ref1.onClose; + expect(onCloseComplete).toBeCalledTimes(1); + }); + }); + + describe('with a currently active confirm', () => { + let confirm1: Promise; + + beforeEach(() => { + confirm1 = modals.openConfirm('confirm 1'); + }); + + it('replaces the current confirm with the new one', () => { + modals.openConfirm('some confirm'); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + }); + + it('resolves the previous confirm promise', async () => { + modals.openConfirm('some confirm'); + expect(await confirm1).toEqual(false); + }); + }); }); describe('ModalRef#close()', () => { diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index cb77c2ec4c88c..ba7887b1afa5c 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -19,7 +19,8 @@ /* eslint-disable max-classes-per-file */ -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n as t } from '@kbn/i18n'; +import { EuiModal, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -57,6 +58,18 @@ class ModalRef implements OverlayRef { } } +/** + * @public + */ +export interface OverlayModalConfirmOptions { + title?: string; + cancelButtonText?: string; + confirmButtonText?: string; + className?: string; + closeButtonAriaLabel?: string; + 'data-test-subj'?: string; +} + /** * APIs to open and manage modal dialogs. * @@ -72,6 +85,14 @@ export interface OverlayModalStart { * @return {@link OverlayRef} A reference to the opened modal. */ open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef; + /** + * Opens a confirmation modal with the given text or mountpoint as a message. + * Returns a Promise resolving to `true` if user confirmed or `false` otherwise. + * + * @param message {@link MountPoint} - string or mountpoint to be used a the confirm message body + * @param options {@link OverlayModalConfirmOptions} - options for the confirm modal + */ + openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; } /** @@ -98,7 +119,7 @@ export class ModalService { return { open: (mount: MountPoint, options: OverlayModalOpenOptions = {}): OverlayRef => { - // If there is an active flyout session close it before opening a new one. + // If there is an active modal, close it before opening a new one. if (this.activeModal) { this.activeModal.close(); this.cleanupDom(); @@ -128,6 +149,65 @@ export class ModalService { return modal; }, + openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => { + // If there is an active modal, close it before opening a new one. + if (this.activeModal) { + this.activeModal.close(); + this.cleanupDom(); + } + + return new Promise((resolve, reject) => { + let resolved = false; + const closeModal = (confirmed: boolean) => { + resolved = true; + modal.close(); + resolve(confirmed); + }; + + const modal = new ModalRef(); + modal.onClose.then(() => { + if (this.activeModal === modal) { + this.cleanupDom(); + } + // modal.close can be called when opening a new modal/confirm, so we need to resolve the promise in that case. + if (!resolved) { + closeModal(false); + } + }); + this.activeModal = modal; + + const props = { + ...options, + children: + typeof message === 'string' ? ( + message + ) : ( + + ), + onCancel: () => closeModal(false), + onConfirm: () => closeModal(true), + cancelButtonText: + options?.cancelButtonText || + t.translate('core.overlays.confirm.cancelButton', { + defaultMessage: 'Cancel', + }), + confirmButtonText: + options?.confirmButtonText || + t.translate('core.overlays.confirm.okButton', { + defaultMessage: 'Confirm', + }), + }; + + render( + + + + + , + targetDomElement + ); + }); + }, }; } diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index 2937ec89bfc74..e29247494034f 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -22,9 +22,11 @@ import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock'; import { overlayModalServiceMock } from './modal/modal_service.mock'; const createStartContractMock = () => { + const overlayStart = overlayModalServiceMock.createStartContract(); const startContract: DeeplyMockedKeys = { openFlyout: overlayFlyoutServiceMock.createStartContract().open, - openModal: overlayModalServiceMock.createStartContract().open, + openModal: overlayStart.open, + openConfirm: overlayStart.openConfirm, banners: overlayBannersServiceMock.createStartContract(), }; return startContract; diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index f628182e965d8..2ff43ba3fbf27 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -50,6 +50,7 @@ export class OverlayService { banners, openFlyout: flyouts.open.bind(flyouts), openModal: modals.open.bind(modals), + openConfirm: modals.openConfirm.bind(modals), }; } } @@ -62,4 +63,6 @@ export interface OverlayStart { openFlyout: OverlayFlyoutStart['open']; /** {@link OverlayModalStart#open} */ openModal: OverlayModalStart['open']; + /** {@link OverlayModalStart#openConfirm} */ + openConfirm: OverlayModalStart['openConfirm']; } diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 848f46605d4de..f146c2452868b 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -96,6 +96,7 @@ export function createPluginSetupContext< return { application: { register: app => deps.application.register(plugin.opaqueId, app), + registerAppUpdater: statusUpdater$ => deps.application.registerAppUpdater(statusUpdater$), registerMountContext: (contextName, provider) => deps.application.registerMountContext(plugin.opaqueId, contextName, provider), }, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 281778f9420dd..cafc7e5887e38 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -279,6 +279,37 @@ describe('PluginsService', () => { expect((contracts.get('pluginA')! as any).setupValue).toEqual(1); expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2); }); + + describe('timeout', () => { + const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + mockPluginInitializers.set( + 'pluginA', + jest.fn(() => ({ + setup: jest.fn(() => new Promise(i => i)), + start: jest.fn(() => ({ value: 1 })), + stop: jest.fn(), + })) + ); + const pluginsService = new PluginsService(mockCoreContext, plugins); + const promise = pluginsService.setup(mockSetupDeps); + + jest.runAllTimers(); // load plugin bundles + await flushPromises(); + jest.runAllTimers(); // setup plugins + + await expect(promise).rejects.toMatchInlineSnapshot( + `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + ); + }); + }); }); describe('#start()', () => { @@ -331,6 +362,34 @@ describe('PluginsService', () => { expect((contracts.get('pluginA')! as any).startValue).toEqual(2); expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3); }); + describe('timeout', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('throws timeout error if "start" was not completed in 30 sec.', async () => { + mockPluginInitializers.set( + 'pluginA', + jest.fn(() => ({ + setup: jest.fn(() => ({ value: 1 })), + start: jest.fn(() => new Promise(i => i)), + stop: jest.fn(), + })) + ); + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + const promise = pluginsService.start(mockStartDeps); + jest.runAllTimers(); + + await expect(promise).rejects.toMatchInlineSnapshot( + `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + ); + }); + }); }); describe('#stop()', () => { diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index c1939a3397647..8e1574d05baf8 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -28,7 +28,9 @@ import { } from './plugin_context'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; import { InjectedPluginMetadata } from '../injected_metadata'; +import { withTimeout } from '../../utils'; +const Sec = 1000; /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; /** @internal */ @@ -110,13 +112,15 @@ export class PluginsService implements CoreService ); - contracts.set( - pluginName, - await plugin.setup( + const contract = await withTimeout({ + promise: plugin.setup( createPluginSetupContext(this.coreContext, deps, plugin), pluginDepContracts - ) - ); + ), + timeout: 30 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, + }); + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -142,13 +146,15 @@ export class PluginsService implements CoreService ); - contracts.set( - pluginName, - await plugin.start( + const contract = await withTimeout({ + promise: plugin.start( createPluginStartContext(this.coreContext, deps, plugin), pluginDepContracts - ) - ); + ), + timeout: 30 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, + }); + contracts.set(pluginName, contract); } // Expose start contracts diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f61741571dc1d..aef689162f45a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1,1114 +1,1179 @@ -## API Report File for "kibana" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { Breadcrumb } from '@elastic/eui'; -import { EuiButtonEmptyProps } from '@elastic/eui'; -import { EuiGlobalToastListToast } from '@elastic/eui'; -import { ExclusiveUnion } from '@elastic/eui'; -import { IconType } from '@elastic/eui'; -import { Observable } from 'rxjs'; -import React from 'react'; -import * as Rx from 'rxjs'; -import { ShallowPromise } from '@kbn/utility-types'; -import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types'; -import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; - -// @public -export interface App extends AppBase { - appRoute?: string; - chromeless?: boolean; - mount: AppMount | AppMountDeprecated; -} - -// @public (undocumented) -export interface AppBase { - capabilities?: Partial; - euiIconType?: string; - icon?: string; - // (undocumented) - id: string; - order?: number; - title: string; - tooltip$?: Observable; -} - -// @public (undocumented) -export interface ApplicationSetup { - register(app: App): void; - // @deprecated - registerMountContext(contextName: T, provider: IContextProvider): void; -} - -// @public (undocumented) -export interface ApplicationStart { - capabilities: RecursiveReadonly; - getUrlForApp(appId: string, options?: { - path?: string; - }): string; - navigateToApp(appId: string, options?: { - path?: string; - state?: any; - }): void; - // @deprecated - registerMountContext(contextName: T, provider: IContextProvider): void; -} - -// @public -export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; - -// @public @deprecated -export interface AppMountContext { - core: { - application: Pick; - chrome: ChromeStart; - docLinks: DocLinksStart; - http: HttpStart; - i18n: I18nStart; - notifications: NotificationsStart; - overlays: OverlayStart; - savedObjects: SavedObjectsStart; - uiSettings: IUiSettingsClient; - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; - }; -} - -// @public @deprecated -export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; - -// @public (undocumented) -export interface AppMountParameters { - appBasePath: string; - element: HTMLElement; -} - -// @public -export type AppUnmount = () => void; - -// @public -export interface Capabilities { - [key: string]: Record>; - catalogue: Record; - management: { - [sectionId: string]: Record; - }; - navLinks: Record; -} - -// @public (undocumented) -export interface ChromeBadge { - // (undocumented) - iconType?: IconType; - // (undocumented) - text: string; - // (undocumented) - tooltip: string; -} - -// @public (undocumented) -export interface ChromeBrand { - // (undocumented) - logo?: string; - // (undocumented) - smallLogo?: string; -} - -// @public (undocumented) -export type ChromeBreadcrumb = Breadcrumb; - -// @public -export interface ChromeDocTitle { - // @internal (undocumented) - __legacy: { - setBaseTitle(baseTitle: string): void; - }; - change(newTitle: string | string[]): void; - reset(): void; -} - -// @public (undocumented) -export interface ChromeHelpExtension { - appName: string; - content?: (element: HTMLDivElement) => () => void; - links?: ChromeHelpExtensionMenuLink[]; -} - -// @public (undocumented) -export type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { - linkType: 'custom'; - content: React.ReactNode; -}; - -// @public (undocumented) -export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { - linkType: 'discuss'; - href: string; -}; - -// @public (undocumented) -export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { - linkType: 'documentation'; - href: string; -}; - -// @public (undocumented) -export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { - linkType: 'github'; - labels: string[]; - title?: string; -}; - -// @public (undocumented) -export type ChromeHelpExtensionMenuLink = ExclusiveUnion>>; - -// @public (undocumented) -export interface ChromeNavControl { - // (undocumented) - mount: MountPoint; - // (undocumented) - order?: number; -} - -// @public -export interface ChromeNavControls { - // @internal (undocumented) - getLeft$(): Observable; - // @internal (undocumented) - getRight$(): Observable; - registerLeft(navControl: ChromeNavControl): void; - registerRight(navControl: ChromeNavControl): void; -} - -// @public (undocumented) -export interface ChromeNavLink { - // @deprecated - readonly active?: boolean; - readonly baseUrl: string; - // @deprecated - readonly disabled?: boolean; - readonly euiIconType?: string; - readonly hidden?: boolean; - readonly icon?: string; - readonly id: string; - // @internal - readonly legacy: boolean; - // @deprecated - readonly linkToLastSubUrl?: boolean; - readonly order?: number; - // @deprecated - readonly subUrlBase?: string; - readonly title: string; - readonly tooltip?: string; - // @deprecated - readonly url?: string; -} - -// @public -export interface ChromeNavLinks { - enableForcedAppSwitcherNavigation(): void; - get(id: string): ChromeNavLink | undefined; - getAll(): Array>; - getForceAppSwitcherNavigation$(): Observable; - getNavLinks$(): Observable>>; - has(id: string): boolean; - showOnly(id: string): void; - update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; -} - -// @public (undocumented) -export type ChromeNavLinkUpdateableFields = Partial>; - -// @public -export interface ChromeRecentlyAccessed { - // Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "basePath" - add(link: string, label: string, id: string): void; - get$(): Observable; - get(): ChromeRecentlyAccessedHistoryItem[]; -} - -// @public (undocumented) -export interface ChromeRecentlyAccessedHistoryItem { - // (undocumented) - id: string; - // (undocumented) - label: string; - // (undocumented) - link: string; -} - -// @public -export interface ChromeStart { - addApplicationClass(className: string): void; - docTitle: ChromeDocTitle; - getApplicationClasses$(): Observable; - getBadge$(): Observable; - getBrand$(): Observable; - getBreadcrumbs$(): Observable; - getHelpExtension$(): Observable; - getIsCollapsed$(): Observable; - getIsVisible$(): Observable; - navControls: ChromeNavControls; - navLinks: ChromeNavLinks; - recentlyAccessed: ChromeRecentlyAccessed; - removeApplicationClass(className: string): void; - setAppTitle(appTitle: string): void; - setBadge(badge?: ChromeBadge): void; - setBrand(brand: ChromeBrand): void; - setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; - setHelpExtension(helpExtension?: ChromeHelpExtension): void; - setHelpSupportUrl(url: string): void; - setIsCollapsed(isCollapsed: boolean): void; - setIsVisible(isVisible: boolean): void; -} - -// @public -export interface ContextSetup { - createContextContainer>(): IContextContainer; -} - -// @internal (undocumented) -export interface CoreContext { - // Warning: (ae-forgotten-export) The symbol "CoreId" needs to be exported by the entry point index.d.ts - // - // (undocumented) - coreId: CoreId; - // (undocumented) - env: { - mode: Readonly; - packageInfo: Readonly; - }; -} - -// @public -export interface CoreSetup { - // (undocumented) - application: ApplicationSetup; - // @deprecated (undocumented) - context: ContextSetup; - // (undocumented) - fatalErrors: FatalErrorsSetup; - getStartServices(): Promise<[CoreStart, TPluginsStart]>; - // (undocumented) - http: HttpSetup; - // @deprecated - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; - // (undocumented) - notifications: NotificationsSetup; - // (undocumented) - uiSettings: IUiSettingsClient; -} - -// @public -export interface CoreStart { - // (undocumented) - application: ApplicationStart; - // (undocumented) - chrome: ChromeStart; - // (undocumented) - docLinks: DocLinksStart; - // (undocumented) - http: HttpStart; - // (undocumented) - i18n: I18nStart; - // @deprecated - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; - // (undocumented) - notifications: NotificationsStart; - // (undocumented) - overlays: OverlayStart; - // (undocumented) - savedObjects: SavedObjectsStart; - // (undocumented) - uiSettings: IUiSettingsClient; -} - -// @internal -export class CoreSystem { - // Warning: (ae-forgotten-export) The symbol "Params" needs to be exported by the entry point index.d.ts - constructor(params: Params); - // (undocumented) - setup(): Promise<{ - fatalErrors: FatalErrorsSetup; - } | undefined>; - // (undocumented) - start(): Promise; - // (undocumented) - stop(): void; - } - -// @public (undocumented) -export interface DocLinksStart { - // (undocumented) - readonly DOC_LINK_VERSION: string; - // (undocumented) - readonly ELASTIC_WEBSITE_URL: string; - // (undocumented) - readonly links: { - readonly filebeat: { - readonly base: string; - readonly installation: string; - readonly configuration: string; - readonly elasticsearchOutput: string; - readonly startup: string; - readonly exportedFields: string; - }; - readonly auditbeat: { - readonly base: string; - }; - readonly metricbeat: { - readonly base: string; - }; - readonly heartbeat: { - readonly base: string; - }; - readonly logstash: { - readonly base: string; - }; - readonly functionbeat: { - readonly base: string; - }; - readonly winlogbeat: { - readonly base: string; - }; - readonly aggs: { - readonly date_histogram: string; - readonly date_range: string; - readonly filter: string; - readonly filters: string; - readonly geohash_grid: string; - readonly histogram: string; - readonly ip_range: string; - readonly range: string; - readonly significant_terms: string; - readonly terms: string; - readonly avg: string; - readonly avg_bucket: string; - readonly max_bucket: string; - readonly min_bucket: string; - readonly sum_bucket: string; - readonly cardinality: string; - readonly count: string; - readonly cumulative_sum: string; - readonly derivative: string; - readonly geo_bounds: string; - readonly geo_centroid: string; - readonly max: string; - readonly median: string; - readonly min: string; - readonly moving_avg: string; - readonly percentile_ranks: string; - readonly serial_diff: string; - readonly std_dev: string; - readonly sum: string; - readonly top_hits: string; - }; - readonly scriptedFields: { - readonly scriptFields: string; - readonly scriptAggs: string; - readonly painless: string; - readonly painlessApi: string; - readonly painlessSyntax: string; - readonly luceneExpressions: string; - }; - readonly indexPatterns: { - readonly loadingData: string; - readonly introduction: string; - }; - readonly kibana: string; - readonly siem: string; - readonly query: { - readonly luceneQuerySyntax: string; - readonly queryDsl: string; - readonly kueryQuerySyntax: string; - }; - readonly date: { - readonly dateMath: string; - }; - }; -} - -// @public (undocumented) -export interface EnvironmentMode { - // (undocumented) - dev: boolean; - // (undocumented) - name: 'development' | 'production'; - // (undocumented) - prod: boolean; -} - -// @public -export interface ErrorToastOptions { - title: string; - toastMessage?: string; -} - -// @public -export interface FatalErrorInfo { - // (undocumented) - message: string; - // (undocumented) - stack: string | undefined; -} - -// @public -export interface FatalErrorsSetup { - add: (error: string | Error, source?: string) => never; - get$: () => Rx.Observable; -} - -// @public -export type HandlerContextType> = T extends HandlerFunction ? U : never; - -// @public -export type HandlerFunction = (context: T, ...args: any[]) => any; - -// @public -export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; - -// @public (undocumented) -export interface HttpErrorRequest { - // (undocumented) - error: Error; - // (undocumented) - request: Request; -} - -// @public (undocumented) -export interface HttpErrorResponse extends IHttpResponse { - // (undocumented) - error: Error | IHttpFetchError; -} - -// @public -export interface HttpFetchOptions extends HttpRequestInit { - asResponse?: boolean; - headers?: HttpHeadersInit; - prependBasePath?: boolean; - query?: HttpFetchQuery; -} - -// @public (undocumented) -export interface HttpFetchQuery { - // (undocumented) - [key: string]: string | number | boolean | undefined; -} - -// @public -export interface HttpHandler { - // (undocumented) - (path: string, options: HttpFetchOptions & { - asResponse: true; - }): Promise>; - // (undocumented) - (path: string, options?: HttpFetchOptions): Promise; -} - -// @public (undocumented) -export interface HttpHeadersInit { - // (undocumented) - [name: string]: any; -} - -// @public -export interface HttpInterceptor { - request?(request: Request, controller: IHttpInterceptController): Promise | Request | void; - requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise | Request | void; - response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; - responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; -} - -// @public -export interface HttpRequestInit { - body?: BodyInit | null; - cache?: RequestCache; - credentials?: RequestCredentials; - // (undocumented) - headers?: HttpHeadersInit; - integrity?: string; - keepalive?: boolean; - method?: string; - mode?: RequestMode; - redirect?: RequestRedirect; - referrer?: string; - referrerPolicy?: ReferrerPolicy; - signal?: AbortSignal | null; - window?: null; -} - -// @public (undocumented) -export interface HttpSetup { - addLoadingCountSource(countSource$: Observable): void; - anonymousPaths: IAnonymousPaths; - basePath: IBasePath; - delete: HttpHandler; - fetch: HttpHandler; - get: HttpHandler; - getLoadingCount$(): Observable; - head: HttpHandler; - intercept(interceptor: HttpInterceptor): () => void; - options: HttpHandler; - patch: HttpHandler; - post: HttpHandler; - put: HttpHandler; -} - -// @public -export type HttpStart = HttpSetup; - -// @public -export interface I18nStart { - Context: ({ children }: { - children: React.ReactNode; - }) => JSX.Element; -} - -// Warning: (ae-missing-release-tag) "IAnonymousPaths" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface IAnonymousPaths { - isAnonymous(path: string): boolean; - register(path: string): void; -} - -// @public -export interface IBasePath { - get: () => string; - prepend: (url: string) => string; - remove: (url: string) => string; -} - -// @public -export interface IContextContainer> { - createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; - registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; -} - -// @public -export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; - -// @public (undocumented) -export interface IHttpFetchError extends Error { - // (undocumented) - readonly body?: any; - // @deprecated (undocumented) - readonly req: Request; - // (undocumented) - readonly request: Request; - // @deprecated (undocumented) - readonly res?: Response; - // (undocumented) - readonly response?: Response; -} - -// @public -export interface IHttpInterceptController { - halt(): void; - halted: boolean; -} - -// @public (undocumented) -export interface IHttpResponse { - readonly body?: TResponseBody; - readonly request: Readonly; - readonly response?: Readonly; -} - -// @public -export interface IHttpResponseInterceptorOverrides { - readonly body?: TResponseBody; - readonly response?: Readonly; -} - -// @public -export type IToasts = Pick; - -// @public -export interface IUiSettingsClient { - get$: (key: string, defaultOverride?: T) => Observable; - get: (key: string, defaultOverride?: T) => T; - getAll: () => Readonly>; - getSaved$: () => Observable<{ - key: string; - newValue: T; - oldValue: T; - }>; - getUpdate$: () => Observable<{ - key: string; - newValue: T; - oldValue: T; - }>; - getUpdateErrors$: () => Observable; - isCustom: (key: string) => boolean; - isDeclared: (key: string) => boolean; - isDefault: (key: string) => boolean; - isOverridden: (key: string) => boolean; - overrideLocalDefault: (key: string, newDefault: any) => void; - remove: (key: string) => Promise; - set: (key: string, value: any) => Promise; -} - -// @public @deprecated -export interface LegacyCoreSetup extends CoreSetup { - // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts - // - // @deprecated (undocumented) - injectedMetadata: InjectedMetadataSetup; -} - -// @public @deprecated -export interface LegacyCoreStart extends CoreStart { - // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts - // - // @deprecated (undocumented) - injectedMetadata: InjectedMetadataStart; -} - -// @public (undocumented) -export interface LegacyNavLink { - // (undocumented) - euiIconType?: string; - // (undocumented) - icon?: string; - // (undocumented) - id: string; - // (undocumented) - order: number; - // (undocumented) - title: string; - // (undocumented) - url: string; -} - -// @public -export type MountPoint = (element: T) => UnmountCallback; - -// @public (undocumented) -export interface NotificationsSetup { - // (undocumented) - toasts: ToastsSetup; -} - -// @public (undocumented) -export interface NotificationsStart { - // (undocumented) - toasts: ToastsStart; -} - -// @public (undocumented) -export interface OverlayBannersStart { - add(mount: MountPoint, priority?: number): string; - // Warning: (ae-forgotten-export) The symbol "OverlayBanner" needs to be exported by the entry point index.d.ts - // - // @internal (undocumented) - get$(): Observable; - // (undocumented) - getComponent(): JSX.Element; - remove(id: string): boolean; - replace(id: string | undefined, mount: MountPoint, priority?: number): string; -} - -// @public -export interface OverlayRef { - close(): Promise; - onClose: Promise; -} - -// @public (undocumented) -export interface OverlayStart { - // (undocumented) - banners: OverlayBannersStart; - // Warning: (ae-forgotten-export) The symbol "OverlayFlyoutStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - openFlyout: OverlayFlyoutStart['open']; - // Warning: (ae-forgotten-export) The symbol "OverlayModalStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - openModal: OverlayModalStart['open']; -} - -// @public (undocumented) -export interface PackageInfo { - // (undocumented) - branch: string; - // (undocumented) - buildNum: number; - // (undocumented) - buildSha: string; - // (undocumented) - dist: boolean; - // (undocumented) - version: string; -} - -// @public -export interface Plugin { - // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; - // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; - // (undocumented) - stop?(): void; -} - -// @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; - -// @public -export interface PluginInitializerContext { - // (undocumented) - readonly config: { - get: () => T; - }; - // (undocumented) - readonly env: { - mode: Readonly; - packageInfo: Readonly; - }; - readonly opaqueId: PluginOpaqueId; -} - -// @public (undocumented) -export type PluginOpaqueId = symbol; - -// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ - [K in keyof T]: RecursiveReadonly; -}> : T; - -// @public (undocumented) -export interface SavedObject { - attributes: T; - // (undocumented) - error?: { - message: string; - statusCode: number; - }; - id: string; - migrationVersion?: SavedObjectsMigrationVersion; - references: SavedObjectReference[]; - type: string; - updated_at?: string; - version?: string; -} - -// @public -export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttributeSingle[]; - -// @public -export interface SavedObjectAttributes { - // (undocumented) - [key: string]: SavedObjectAttribute; -} - -// @public -export type SavedObjectAttributeSingle = string | number | boolean | null | undefined | SavedObjectAttributes; - -// @public -export interface SavedObjectReference { - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - type: string; -} - -// @public (undocumented) -export interface SavedObjectsBaseOptions { - namespace?: string; -} - -// @public (undocumented) -export interface SavedObjectsBatchResponse { - // (undocumented) - savedObjects: Array>; -} - -// @public (undocumented) -export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { - // (undocumented) - attributes: T; - // (undocumented) - type: string; -} - -// @public (undocumented) -export interface SavedObjectsBulkCreateOptions { - overwrite?: boolean; -} - -// @public (undocumented) -export interface SavedObjectsBulkUpdateObject { - // (undocumented) - attributes: T; - // (undocumented) - id: string; - // (undocumented) - references?: SavedObjectReference[]; - // (undocumented) - type: string; - // (undocumented) - version?: string; -} - -// @public (undocumented) -export interface SavedObjectsBulkUpdateOptions { - // (undocumented) - namespace?: string; -} - -// @public -export class SavedObjectsClient { - // @internal - constructor(http: HttpSetup); - bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; - bulkGet: (objects?: { - id: string; - type: string; - }[]) => Promise>; - bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; - create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; - delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; - get: (type: string, id: string) => Promise>; - update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; -} - -// @public -export type SavedObjectsClientContract = PublicMethodsOf; - -// @public (undocumented) -export interface SavedObjectsCreateOptions { - id?: string; - migrationVersion?: SavedObjectsMigrationVersion; - overwrite?: boolean; - // (undocumented) - references?: SavedObjectReference[]; -} - -// @public (undocumented) -export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { - // (undocumented) - defaultSearchOperator?: 'AND' | 'OR'; - fields?: string[]; - // (undocumented) - filter?: string; - // (undocumented) - hasReference?: { - type: string; - id: string; - }; - // (undocumented) - page?: number; - // (undocumented) - perPage?: number; - search?: string; - searchFields?: string[]; - // (undocumented) - sortField?: string; - // (undocumented) - sortOrder?: string; - // (undocumented) - type: string | string[]; -} - -// @public -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { - // (undocumented) - page: number; - // (undocumented) - perPage: number; - // (undocumented) - total: number; -} - -// @public -export interface SavedObjectsImportConflictError { - // (undocumented) - type: 'conflict'; -} - -// @public -export interface SavedObjectsImportError { - // (undocumented) - error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; - // (undocumented) - id: string; - // (undocumented) - title?: string; - // (undocumented) - type: string; -} - -// @public -export interface SavedObjectsImportMissingReferencesError { - // (undocumented) - blocking: Array<{ - type: string; - id: string; - }>; - // (undocumented) - references: Array<{ - type: string; - id: string; - }>; - // (undocumented) - type: 'missing_references'; -} - -// @public -export interface SavedObjectsImportResponse { - // (undocumented) - errors?: SavedObjectsImportError[]; - // (undocumented) - success: boolean; - // (undocumented) - successCount: number; -} - -// @public -export interface SavedObjectsImportRetry { - // (undocumented) - id: string; - // (undocumented) - overwrite: boolean; - // (undocumented) - replaceReferences: Array<{ - type: string; - from: string; - to: string; - }>; - // (undocumented) - type: string; -} - -// @public -export interface SavedObjectsImportUnknownError { - // (undocumented) - message: string; - // (undocumented) - statusCode: number; - // (undocumented) - type: 'unknown'; -} - -// @public -export interface SavedObjectsImportUnsupportedTypeError { - // (undocumented) - type: 'unsupported_type'; -} - -// @public -export interface SavedObjectsMigrationVersion { - // (undocumented) - [pluginName: string]: string; -} - -// @public (undocumented) -export interface SavedObjectsStart { - // (undocumented) - client: SavedObjectsClientContract; -} - -// @public (undocumented) -export interface SavedObjectsUpdateOptions { - migrationVersion?: SavedObjectsMigrationVersion; - // (undocumented) - references?: SavedObjectReference[]; - // (undocumented) - version?: string; -} - -// @public -export class SimpleSavedObject { - constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); - // (undocumented) - attributes: T; - // (undocumented) - delete(): Promise<{}>; - // (undocumented) - error: SavedObject['error']; - // (undocumented) - get(key: string): any; - // (undocumented) - has(key: string): boolean; - // (undocumented) - id: SavedObject['id']; - // (undocumented) - migrationVersion: SavedObject['migrationVersion']; - // (undocumented) - references: SavedObject['references']; - // (undocumented) - save(): Promise>; - // (undocumented) - set(key: string, value: any): T; - // (undocumented) - type: SavedObject['type']; - // (undocumented) - _version?: SavedObject['version']; -} - -// Warning: (ae-missing-release-tag) "Toast" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type Toast = ToastInputFields & { - id: string; -}; - -// @public -export type ToastInput = string | ToastInputFields; - -// @public -export type ToastInputFields = Pick> & { - title?: string | MountPoint; - text?: string | MountPoint; -}; - -// @public -export class ToastsApi implements IToasts { - constructor(deps: { - uiSettings: IUiSettingsClient; - }); - add(toastOrTitle: ToastInput): Toast; - addDanger(toastOrTitle: ToastInput): Toast; - addError(error: Error, options: ErrorToastOptions): Toast; - addSuccess(toastOrTitle: ToastInput): Toast; - addWarning(toastOrTitle: ToastInput): Toast; - get$(): Rx.Observable; - remove(toastOrId: Toast | string): void; - // @internal (undocumented) - start({ overlays, i18n }: { - overlays: OverlayStart; - i18n: I18nStart; - }): void; - } - -// @public (undocumented) -export type ToastsSetup = IToasts; - -// @public (undocumented) -export type ToastsStart = IToasts; - -// @public (undocumented) -export interface UiSettingsState { - // (undocumented) - [key: string]: UiSettingsParams_2 & UserProvidedValues_2; -} - -// @public -export type UnmountCallback = () => void; - - -``` +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Breadcrumb } from '@elastic/eui'; +import { EuiButtonEmptyProps } from '@elastic/eui'; +import { EuiGlobalToastListToast } from '@elastic/eui'; +import { ExclusiveUnion } from '@elastic/eui'; +import { IconType } from '@elastic/eui'; +import { Observable } from 'rxjs'; +import React from 'react'; +import * as Rx from 'rxjs'; +import { ShallowPromise } from '@kbn/utility-types'; +import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types'; +import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; + +// @public +export interface App extends AppBase { + appRoute?: string; + chromeless?: boolean; + mount: AppMount | AppMountDeprecated; +} + +// @public (undocumented) +export interface AppBase { + capabilities?: Partial; + chromeless?: boolean; + euiIconType?: string; + icon?: string; + id: string; + // @internal + legacy?: boolean; + navLinkStatus?: AppNavLinkStatus; + order?: number; + status?: AppStatus; + title: string; + tooltip?: string; + updater$?: Observable; +} + +// @public +export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction; + +// @public +export enum AppLeaveActionType { + // (undocumented) + confirm = "confirm", + // (undocumented) + default = "default" +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppLeaveActionFactory" +// +// @public +export interface AppLeaveConfirmAction { + // (undocumented) + text: string; + // (undocumented) + title?: string; + // (undocumented) + type: AppLeaveActionType.confirm; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppLeaveActionFactory" +// +// @public +export interface AppLeaveDefaultAction { + // (undocumented) + type: AppLeaveActionType.default; +} + +// Warning: (ae-forgotten-export) The symbol "AppLeaveActionFactory" needs to be exported by the entry point index.d.ts +// +// @public +export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction; + +// @public (undocumented) +export interface ApplicationSetup { + register(app: App): void; + registerAppUpdater(appUpdater$: Observable): void; + // @deprecated + registerMountContext(contextName: T, provider: IContextProvider): void; +} + +// @public (undocumented) +export interface ApplicationStart { + capabilities: RecursiveReadonly; + getUrlForApp(appId: string, options?: { + path?: string; + }): string; + navigateToApp(appId: string, options?: { + path?: string; + state?: any; + }): Promise; + // @deprecated + registerMountContext(contextName: T, provider: IContextProvider): void; +} + +// @public +export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; + +// @public @deprecated +export interface AppMountContext { + core: { + application: Pick; + chrome: ChromeStart; + docLinks: DocLinksStart; + http: HttpStart; + i18n: I18nStart; + notifications: NotificationsStart; + overlays: OverlayStart; + savedObjects: SavedObjectsStart; + uiSettings: IUiSettingsClient; + injectedMetadata: { + getInjectedVar: (name: string, defaultValue?: any) => unknown; + }; + }; +} + +// @public @deprecated +export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + +// @public (undocumented) +export interface AppMountParameters { + appBasePath: string; + element: HTMLElement; + onAppLeave: (handler: AppLeaveHandler) => void; +} + +// @public +export enum AppNavLinkStatus { + default = 0, + disabled = 2, + hidden = 3, + visible = 1 +} + +// @public +export enum AppStatus { + accessible = 0, + inaccessible = 1 +} + +// @public +export type AppUnmount = () => void; + +// @public +export type AppUpdatableFields = Pick; + +// @public +export type AppUpdater = (app: AppBase) => Partial | undefined; + +// @public +export interface Capabilities { + [key: string]: Record>; + catalogue: Record; + management: { + [sectionId: string]: Record; + }; + navLinks: Record; +} + +// @public (undocumented) +export interface ChromeBadge { + // (undocumented) + iconType?: IconType; + // (undocumented) + text: string; + // (undocumented) + tooltip: string; +} + +// @public (undocumented) +export interface ChromeBrand { + // (undocumented) + logo?: string; + // (undocumented) + smallLogo?: string; +} + +// @public (undocumented) +export type ChromeBreadcrumb = Breadcrumb; + +// @public +export interface ChromeDocTitle { + // @internal (undocumented) + __legacy: { + setBaseTitle(baseTitle: string): void; + }; + change(newTitle: string | string[]): void; + reset(): void; +} + +// @public (undocumented) +export interface ChromeHelpExtension { + appName: string; + content?: (element: HTMLDivElement) => () => void; + links?: ChromeHelpExtensionMenuLink[]; +} + +// @public (undocumented) +export type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { + linkType: 'custom'; + content: React.ReactNode; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { + linkType: 'discuss'; + href: string; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { + linkType: 'documentation'; + href: string; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { + linkType: 'github'; + labels: string[]; + title?: string; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuLink = ExclusiveUnion>>; + +// @public (undocumented) +export interface ChromeNavControl { + // (undocumented) + mount: MountPoint; + // (undocumented) + order?: number; +} + +// @public +export interface ChromeNavControls { + // @internal (undocumented) + getLeft$(): Observable; + // @internal (undocumented) + getRight$(): Observable; + registerLeft(navControl: ChromeNavControl): void; + registerRight(navControl: ChromeNavControl): void; +} + +// @public (undocumented) +export interface ChromeNavLink { + // @deprecated + readonly active?: boolean; + readonly baseUrl: string; + // @deprecated + readonly disabled?: boolean; + readonly euiIconType?: string; + readonly hidden?: boolean; + readonly icon?: string; + readonly id: string; + // @internal + readonly legacy: boolean; + // @deprecated + readonly linkToLastSubUrl?: boolean; + readonly order?: number; + // @deprecated + readonly subUrlBase?: string; + readonly title: string; + readonly tooltip?: string; + // @deprecated + readonly url?: string; +} + +// @public +export interface ChromeNavLinks { + enableForcedAppSwitcherNavigation(): void; + get(id: string): ChromeNavLink | undefined; + getAll(): Array>; + getForceAppSwitcherNavigation$(): Observable; + getNavLinks$(): Observable>>; + has(id: string): boolean; + showOnly(id: string): void; + update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; +} + +// @public (undocumented) +export type ChromeNavLinkUpdateableFields = Partial>; + +// @public +export interface ChromeRecentlyAccessed { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "basePath" + add(link: string, label: string, id: string): void; + get$(): Observable; + get(): ChromeRecentlyAccessedHistoryItem[]; +} + +// @public (undocumented) +export interface ChromeRecentlyAccessedHistoryItem { + // (undocumented) + id: string; + // (undocumented) + label: string; + // (undocumented) + link: string; +} + +// @public +export interface ChromeStart { + addApplicationClass(className: string): void; + docTitle: ChromeDocTitle; + getApplicationClasses$(): Observable; + getBadge$(): Observable; + getBrand$(): Observable; + getBreadcrumbs$(): Observable; + getHelpExtension$(): Observable; + getIsCollapsed$(): Observable; + getIsVisible$(): Observable; + navControls: ChromeNavControls; + navLinks: ChromeNavLinks; + recentlyAccessed: ChromeRecentlyAccessed; + removeApplicationClass(className: string): void; + setAppTitle(appTitle: string): void; + setBadge(badge?: ChromeBadge): void; + setBrand(brand: ChromeBrand): void; + setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + setHelpExtension(helpExtension?: ChromeHelpExtension): void; + setHelpSupportUrl(url: string): void; + setIsCollapsed(isCollapsed: boolean): void; + setIsVisible(isVisible: boolean): void; +} + +// @public +export interface ContextSetup { + createContextContainer>(): IContextContainer; +} + +// @internal (undocumented) +export interface CoreContext { + // Warning: (ae-forgotten-export) The symbol "CoreId" needs to be exported by the entry point index.d.ts + // + // (undocumented) + coreId: CoreId; + // (undocumented) + env: { + mode: Readonly; + packageInfo: Readonly; + }; +} + +// @public +export interface CoreSetup { + // (undocumented) + application: ApplicationSetup; + // @deprecated (undocumented) + context: ContextSetup; + // (undocumented) + fatalErrors: FatalErrorsSetup; + getStartServices(): Promise<[CoreStart, TPluginsStart]>; + // (undocumented) + http: HttpSetup; + // @deprecated + injectedMetadata: { + getInjectedVar: (name: string, defaultValue?: any) => unknown; + }; + // (undocumented) + notifications: NotificationsSetup; + // (undocumented) + uiSettings: IUiSettingsClient; +} + +// @public +export interface CoreStart { + // (undocumented) + application: ApplicationStart; + // (undocumented) + chrome: ChromeStart; + // (undocumented) + docLinks: DocLinksStart; + // (undocumented) + http: HttpStart; + // (undocumented) + i18n: I18nStart; + // @deprecated + injectedMetadata: { + getInjectedVar: (name: string, defaultValue?: any) => unknown; + }; + // (undocumented) + notifications: NotificationsStart; + // (undocumented) + overlays: OverlayStart; + // (undocumented) + savedObjects: SavedObjectsStart; + // (undocumented) + uiSettings: IUiSettingsClient; +} + +// @internal +export class CoreSystem { + // Warning: (ae-forgotten-export) The symbol "Params" needs to be exported by the entry point index.d.ts + constructor(params: Params); + // (undocumented) + setup(): Promise<{ + fatalErrors: FatalErrorsSetup; + } | undefined>; + // (undocumented) + start(): Promise; + // (undocumented) + stop(): void; + } + +// @public (undocumented) +export interface DocLinksStart { + // (undocumented) + readonly DOC_LINK_VERSION: string; + // (undocumented) + readonly ELASTIC_WEBSITE_URL: string; + // (undocumented) + readonly links: { + readonly filebeat: { + readonly base: string; + readonly installation: string; + readonly configuration: string; + readonly elasticsearchOutput: string; + readonly startup: string; + readonly exportedFields: string; + }; + readonly auditbeat: { + readonly base: string; + }; + readonly metricbeat: { + readonly base: string; + }; + readonly heartbeat: { + readonly base: string; + }; + readonly logstash: { + readonly base: string; + }; + readonly functionbeat: { + readonly base: string; + }; + readonly winlogbeat: { + readonly base: string; + }; + readonly aggs: { + readonly date_histogram: string; + readonly date_range: string; + readonly filter: string; + readonly filters: string; + readonly geohash_grid: string; + readonly histogram: string; + readonly ip_range: string; + readonly range: string; + readonly significant_terms: string; + readonly terms: string; + readonly avg: string; + readonly avg_bucket: string; + readonly max_bucket: string; + readonly min_bucket: string; + readonly sum_bucket: string; + readonly cardinality: string; + readonly count: string; + readonly cumulative_sum: string; + readonly derivative: string; + readonly geo_bounds: string; + readonly geo_centroid: string; + readonly max: string; + readonly median: string; + readonly min: string; + readonly moving_avg: string; + readonly percentile_ranks: string; + readonly serial_diff: string; + readonly std_dev: string; + readonly sum: string; + readonly top_hits: string; + }; + readonly scriptedFields: { + readonly scriptFields: string; + readonly scriptAggs: string; + readonly painless: string; + readonly painlessApi: string; + readonly painlessSyntax: string; + readonly luceneExpressions: string; + }; + readonly indexPatterns: { + readonly loadingData: string; + readonly introduction: string; + }; + readonly kibana: string; + readonly siem: string; + readonly query: { + readonly luceneQuerySyntax: string; + readonly queryDsl: string; + readonly kueryQuerySyntax: string; + }; + readonly date: { + readonly dateMath: string; + }; + }; +} + +// @public (undocumented) +export interface EnvironmentMode { + // (undocumented) + dev: boolean; + // (undocumented) + name: 'development' | 'production'; + // (undocumented) + prod: boolean; +} + +// @public +export interface ErrorToastOptions { + title: string; + toastMessage?: string; +} + +// @public +export interface FatalErrorInfo { + // (undocumented) + message: string; + // (undocumented) + stack: string | undefined; +} + +// @public +export interface FatalErrorsSetup { + add: (error: string | Error, source?: string) => never; + get$: () => Rx.Observable; +} + +// @public +export type HandlerContextType> = T extends HandlerFunction ? U : never; + +// @public +export type HandlerFunction = (context: T, ...args: any[]) => any; + +// @public +export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; + +// @public (undocumented) +export interface HttpErrorRequest { + // (undocumented) + error: Error; + // (undocumented) + request: Request; +} + +// @public (undocumented) +export interface HttpErrorResponse extends IHttpResponse { + // (undocumented) + error: Error | IHttpFetchError; +} + +// @public +export interface HttpFetchOptions extends HttpRequestInit { + asResponse?: boolean; + headers?: HttpHeadersInit; + prependBasePath?: boolean; + query?: HttpFetchQuery; +} + +// @public (undocumented) +export interface HttpFetchQuery { + // (undocumented) + [key: string]: string | number | boolean | undefined; +} + +// @public +export interface HttpHandler { + // (undocumented) + (path: string, options: HttpFetchOptions & { + asResponse: true; + }): Promise>; + // (undocumented) + (path: string, options?: HttpFetchOptions): Promise; +} + +// @public (undocumented) +export interface HttpHeadersInit { + // (undocumented) + [name: string]: any; +} + +// @public +export interface HttpInterceptor { + request?(request: Request, controller: IHttpInterceptController): Promise | Request | void; + requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise | Request | void; + response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; + responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; +} + +// @public +export interface HttpRequestInit { + body?: BodyInit | null; + cache?: RequestCache; + credentials?: RequestCredentials; + // (undocumented) + headers?: HttpHeadersInit; + integrity?: string; + keepalive?: boolean; + method?: string; + mode?: RequestMode; + redirect?: RequestRedirect; + referrer?: string; + referrerPolicy?: ReferrerPolicy; + signal?: AbortSignal | null; + window?: null; +} + +// @public (undocumented) +export interface HttpSetup { + addLoadingCountSource(countSource$: Observable): void; + anonymousPaths: IAnonymousPaths; + basePath: IBasePath; + delete: HttpHandler; + fetch: HttpHandler; + get: HttpHandler; + getLoadingCount$(): Observable; + head: HttpHandler; + intercept(interceptor: HttpInterceptor): () => void; + options: HttpHandler; + patch: HttpHandler; + post: HttpHandler; + put: HttpHandler; +} + +// @public +export type HttpStart = HttpSetup; + +// @public +export interface I18nStart { + Context: ({ children }: { + children: React.ReactNode; + }) => JSX.Element; +} + +// Warning: (ae-missing-release-tag) "IAnonymousPaths" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface IAnonymousPaths { + isAnonymous(path: string): boolean; + register(path: string): void; +} + +// @public +export interface IBasePath { + get: () => string; + prepend: (url: string) => string; + remove: (url: string) => string; +} + +// @public +export interface IContextContainer> { + createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; + registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +} + +// @public +export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; + +// @public (undocumented) +export interface IHttpFetchError extends Error { + // (undocumented) + readonly body?: any; + // @deprecated (undocumented) + readonly req: Request; + // (undocumented) + readonly request: Request; + // @deprecated (undocumented) + readonly res?: Response; + // (undocumented) + readonly response?: Response; +} + +// @public +export interface IHttpInterceptController { + halt(): void; + halted: boolean; +} + +// @public (undocumented) +export interface IHttpResponse { + readonly body?: TResponseBody; + readonly request: Readonly; + readonly response?: Readonly; +} + +// @public +export interface IHttpResponseInterceptorOverrides { + readonly body?: TResponseBody; + readonly response?: Readonly; +} + +// @public +export type IToasts = Pick; + +// @public +export interface IUiSettingsClient { + get$: (key: string, defaultOverride?: T) => Observable; + get: (key: string, defaultOverride?: T) => T; + getAll: () => Readonly>; + getSaved$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; + getUpdate$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; + getUpdateErrors$: () => Observable; + isCustom: (key: string) => boolean; + isDeclared: (key: string) => boolean; + isDefault: (key: string) => boolean; + isOverridden: (key: string) => boolean; + overrideLocalDefault: (key: string, newDefault: any) => void; + remove: (key: string) => Promise; + set: (key: string, value: any) => Promise; +} + +// @public @deprecated +export interface LegacyCoreSetup extends CoreSetup { + // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts + // + // @deprecated (undocumented) + injectedMetadata: InjectedMetadataSetup; +} + +// @public @deprecated +export interface LegacyCoreStart extends CoreStart { + // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts + // + // @deprecated (undocumented) + injectedMetadata: InjectedMetadataStart; +} + +// @public (undocumented) +export interface LegacyNavLink { + // (undocumented) + euiIconType?: string; + // (undocumented) + icon?: string; + // (undocumented) + id: string; + // (undocumented) + order: number; + // (undocumented) + title: string; + // (undocumented) + url: string; +} + +// @public +export type MountPoint = (element: T) => UnmountCallback; + +// @public (undocumented) +export interface NotificationsSetup { + // (undocumented) + toasts: ToastsSetup; +} + +// @public (undocumented) +export interface NotificationsStart { + // (undocumented) + toasts: ToastsStart; +} + +// @public (undocumented) +export interface OverlayBannersStart { + add(mount: MountPoint, priority?: number): string; + // Warning: (ae-forgotten-export) The symbol "OverlayBanner" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + get$(): Observable; + // (undocumented) + getComponent(): JSX.Element; + remove(id: string): boolean; + replace(id: string | undefined, mount: MountPoint, priority?: number): string; +} + +// @public +export interface OverlayRef { + close(): Promise; + onClose: Promise; +} + +// @public (undocumented) +export interface OverlayStart { + // (undocumented) + banners: OverlayBannersStart; + // (undocumented) + openConfirm: OverlayModalStart['openConfirm']; + // Warning: (ae-forgotten-export) The symbol "OverlayFlyoutStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + openFlyout: OverlayFlyoutStart['open']; + // Warning: (ae-forgotten-export) The symbol "OverlayModalStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + openModal: OverlayModalStart['open']; +} + +// @public (undocumented) +export interface PackageInfo { + // (undocumented) + branch: string; + // (undocumented) + buildNum: number; + // (undocumented) + buildSha: string; + // (undocumented) + dist: boolean; + // (undocumented) + version: string; +} + +// @public +export interface Plugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + +// @public +export type PluginInitializer = (core: PluginInitializerContext) => Plugin; + +// @public +export interface PluginInitializerContext { + // (undocumented) + readonly config: { + get: () => T; + }; + // (undocumented) + readonly env: { + mode: Readonly; + packageInfo: Readonly; + }; + readonly opaqueId: PluginOpaqueId; +} + +// @public (undocumented) +export type PluginOpaqueId = symbol; + +// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ + [K in keyof T]: RecursiveReadonly; +}> : T; + +// @public (undocumented) +export interface SavedObject { + attributes: T; + // (undocumented) + error?: { + message: string; + statusCode: number; + }; + id: string; + migrationVersion?: SavedObjectsMigrationVersion; + references: SavedObjectReference[]; + type: string; + updated_at?: string; + version?: string; +} + +// @public +export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttributeSingle[]; + +// @public +export interface SavedObjectAttributes { + // (undocumented) + [key: string]: SavedObjectAttribute; +} + +// @public +export type SavedObjectAttributeSingle = string | number | boolean | null | undefined | SavedObjectAttributes; + +// @public +export interface SavedObjectReference { + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBaseOptions { + namespace?: string; +} + +// @public (undocumented) +export interface SavedObjectsBatchResponse { + // (undocumented) + savedObjects: Array>; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { + // (undocumented) + attributes: T; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateOptions { + overwrite?: boolean; +} + +// @public (undocumented) +export interface SavedObjectsBulkUpdateObject { + // (undocumented) + attributes: T; + // (undocumented) + id: string; + // (undocumented) + references?: SavedObjectReference[]; + // (undocumented) + type: string; + // (undocumented) + version?: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkUpdateOptions { + // (undocumented) + namespace?: string; +} + +// @public +export class SavedObjectsClient { + // @internal + constructor(http: HttpSetup); + bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; + bulkGet: (objects?: { + id: string; + type: string; + }[]) => Promise>; + bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; + create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; + delete: (type: string, id: string) => Promise<{}>; + find: (options: Pick) => Promise>; + get: (type: string, id: string) => Promise>; + update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +} + +// @public +export type SavedObjectsClientContract = PublicMethodsOf; + +// @public (undocumented) +export interface SavedObjectsCreateOptions { + id?: string; + migrationVersion?: SavedObjectsMigrationVersion; + overwrite?: boolean; + // (undocumented) + references?: SavedObjectReference[]; +} + +// @public (undocumented) +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // (undocumented) + defaultSearchOperator?: 'AND' | 'OR'; + fields?: string[]; + // (undocumented) + filter?: string; + // (undocumented) + hasReference?: { + type: string; + id: string; + }; + // (undocumented) + page?: number; + // (undocumented) + perPage?: number; + search?: string; + searchFields?: string[]; + // (undocumented) + sortField?: string; + // (undocumented) + sortOrder?: string; + // (undocumented) + type: string | string[]; +} + +// @public +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { + // (undocumented) + page: number; + // (undocumented) + perPage: number; + // (undocumented) + total: number; +} + +// @public +export interface SavedObjectsImportConflictError { + // (undocumented) + type: 'conflict'; +} + +// @public +export interface SavedObjectsImportError { + // (undocumented) + error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; + // (undocumented) + id: string; + // (undocumented) + title?: string; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsImportMissingReferencesError { + // (undocumented) + blocking: Array<{ + type: string; + id: string; + }>; + // (undocumented) + references: Array<{ + type: string; + id: string; + }>; + // (undocumented) + type: 'missing_references'; +} + +// @public +export interface SavedObjectsImportResponse { + // (undocumented) + errors?: SavedObjectsImportError[]; + // (undocumented) + success: boolean; + // (undocumented) + successCount: number; +} + +// @public +export interface SavedObjectsImportRetry { + // (undocumented) + id: string; + // (undocumented) + overwrite: boolean; + // (undocumented) + replaceReferences: Array<{ + type: string; + from: string; + to: string; + }>; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsImportUnknownError { + // (undocumented) + message: string; + // (undocumented) + statusCode: number; + // (undocumented) + type: 'unknown'; +} + +// @public +export interface SavedObjectsImportUnsupportedTypeError { + // (undocumented) + type: 'unsupported_type'; +} + +// @public +export interface SavedObjectsMigrationVersion { + // (undocumented) + [pluginName: string]: string; +} + +// @public (undocumented) +export interface SavedObjectsStart { + // (undocumented) + client: SavedObjectsClientContract; +} + +// @public (undocumented) +export interface SavedObjectsUpdateOptions { + migrationVersion?: SavedObjectsMigrationVersion; + // (undocumented) + references?: SavedObjectReference[]; + // (undocumented) + version?: string; +} + +// @public +export class SimpleSavedObject { + constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); + // (undocumented) + attributes: T; + // (undocumented) + delete(): Promise<{}>; + // (undocumented) + error: SavedObject['error']; + // (undocumented) + get(key: string): any; + // (undocumented) + has(key: string): boolean; + // (undocumented) + id: SavedObject['id']; + // (undocumented) + migrationVersion: SavedObject['migrationVersion']; + // (undocumented) + references: SavedObject['references']; + // (undocumented) + save(): Promise>; + // (undocumented) + set(key: string, value: any): T; + // (undocumented) + type: SavedObject['type']; + // (undocumented) + _version?: SavedObject['version']; +} + +// Warning: (ae-missing-release-tag) "Toast" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Toast = ToastInputFields & { + id: string; +}; + +// @public +export type ToastInput = string | ToastInputFields; + +// @public +export type ToastInputFields = Pick> & { + title?: string | MountPoint; + text?: string | MountPoint; +}; + +// @public +export class ToastsApi implements IToasts { + constructor(deps: { + uiSettings: IUiSettingsClient; + }); + add(toastOrTitle: ToastInput): Toast; + addDanger(toastOrTitle: ToastInput): Toast; + addError(error: Error, options: ErrorToastOptions): Toast; + addSuccess(toastOrTitle: ToastInput): Toast; + addWarning(toastOrTitle: ToastInput): Toast; + get$(): Rx.Observable; + remove(toastOrId: Toast | string): void; + // @internal (undocumented) + start({ overlays, i18n }: { + overlays: OverlayStart; + i18n: I18nStart; + }): void; + } + +// @public (undocumented) +export type ToastsSetup = IToasts; + +// @public (undocumented) +export type ToastsStart = IToasts; + +// @public (undocumented) +export interface UiSettingsState { + // (undocumented) + [key: string]: UiSettingsParams_2 & UserProvidedValues_2; +} + +// @public +export type UnmountCallback = () => void; + + +``` diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts index d43ab9d546ed2..2352677b8d3e0 100644 --- a/src/core/server/elasticsearch/cluster_client.ts +++ b/src/core/server/elasticsearch/cluster_client.ts @@ -89,15 +89,35 @@ export interface FakeRequest { } /** - * Represents an Elasticsearch cluster API client and allows to call API on behalf - * of the internal Kibana user and the actual user that is derived from the request - * headers (via `asScoped(...)`). + * Represents an Elasticsearch cluster API client created by the platform. + * It allows to call API on behalf of the internal Kibana user and + * the actual user that is derived from the request headers (via `asScoped(...)`). * * See {@link ClusterClient}. * * @public */ -export type IClusterClient = Pick; +export type IClusterClient = Pick; + +/** + * Represents an Elasticsearch cluster API client created by a plugin. + * It allows to call API on behalf of the internal Kibana user and + * the actual user that is derived from the request headers (via `asScoped(...)`). + * + * See {@link ClusterClient}. + * + * @public + */ +export type ICustomClusterClient = Pick; + +/** + A user credentials container. + * It accommodates the necessary auth credentials to impersonate the current user. + * + * @public + * See {@link KibanaRequest}. + */ +export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; /** * {@inheritDoc IClusterClient} @@ -174,7 +194,7 @@ export class ClusterClient implements IClusterClient { * @param request - Request the `IScopedClusterClient` instance will be scoped to. * Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform */ - public asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): IScopedClusterClient { + public asScoped(request?: ScopeableRequest): IScopedClusterClient { // It'd have been quite expensive to create and configure client for every incoming // request since it involves parsing of the config, reading of the SSL certificate and // key files etc. Moreover scoped client needs two Elasticsearch JS clients at the same diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts index 64fb41cb3e4e5..20c10459e0e8a 100644 --- a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts @@ -17,8 +17,6 @@ * under the License. */ -import { mockReadFileSync } from './elasticsearch_client_config.test.mocks'; - import { duration } from 'moment'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { @@ -66,8 +64,6 @@ Object { }); test('parses fully specified config', () => { - mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); - const elasticsearchConfig: ElasticsearchClientConfig = { apiVersion: 'v7.0.0', customHeaders: { xsrf: 'something' }, @@ -87,9 +83,9 @@ test('parses fully specified config', () => { sniffInterval: 11223344, ssl: { verificationMode: 'certificate', - certificateAuthorities: ['ca-path-1', 'ca-path-2'], - certificate: 'certificate-path', - key: 'key-path', + certificateAuthorities: ['content-of-ca-path-1', 'content-of-ca-path-2'], + certificate: 'content-of-certificate-path', + key: 'content-of-key-path', keyPassphrase: 'key-pass', alwaysPresentCertificate: true, }, @@ -497,6 +493,7 @@ Object { "sniffOnConnectionFault": true, "sniffOnStart": true, "ssl": Object { + "ca": undefined, "rejectUnauthorized": false, }, } @@ -541,6 +538,7 @@ Object { "sniffOnConnectionFault": true, "sniffOnStart": true, "ssl": Object { + "ca": undefined, "checkServerIdentity": [Function], "rejectUnauthorized": true, }, @@ -581,6 +579,7 @@ Object { "sniffOnConnectionFault": true, "sniffOnStart": true, "ssl": Object { + "ca": undefined, "rejectUnauthorized": true, }, } @@ -606,8 +605,6 @@ Object { }); test('#ignoreCertAndKey = true', () => { - mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); - expect( parseElasticsearchClientConfig( { @@ -620,9 +617,9 @@ Object { requestHeadersWhitelist: [], ssl: { verificationMode: 'certificate', - certificateAuthorities: ['ca-path'], - certificate: 'certificate-path', - key: 'key-path', + certificateAuthorities: ['content-of-ca-path'], + certificate: 'content-of-certificate-path', + key: 'content-of-key-path', keyPassphrase: 'key-pass', alwaysPresentCertificate: true, }, diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.ts b/src/core/server/elasticsearch/elasticsearch_client_config.ts index dcc09f711abbe..287d835c40351 100644 --- a/src/core/server/elasticsearch/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_client_config.ts @@ -18,7 +18,6 @@ */ import { ConfigOptions } from 'elasticsearch'; -import { readFileSync } from 'fs'; import { cloneDeep } from 'lodash'; import { Duration } from 'moment'; import { checkServerIdentity } from 'tls'; @@ -165,18 +164,12 @@ export function parseElasticsearchClientConfig( throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); } - const readFile = (file: string) => readFileSync(file, 'utf8'); - if ( - config.ssl.certificateAuthorities !== undefined && - config.ssl.certificateAuthorities.length > 0 - ) { - esClientConfig.ssl.ca = config.ssl.certificateAuthorities.map(readFile); - } + esClientConfig.ssl.ca = config.ssl.certificateAuthorities; // Add client certificate and key if required by elasticsearch if (!ignoreCertAndKey && config.ssl.certificate && config.ssl.key) { - esClientConfig.ssl.cert = readFile(config.ssl.certificate); - esClientConfig.ssl.key = readFile(config.ssl.key); + esClientConfig.ssl.cert = config.ssl.certificate; + esClientConfig.ssl.key = config.ssl.key; esClientConfig.ssl.passphrase = config.ssl.keyPassphrase; } diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts similarity index 70% rename from src/legacy/core_plugins/kbn_doc_views/public/views/table.ts rename to src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts index f90840f195003..d908fdbfd2e80 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts @@ -17,15 +17,12 @@ * under the License. */ -import _ from 'lodash'; -import { addDocView } from 'ui/registry/doc_views'; -import { i18n } from '@kbn/i18n'; -import { DocViewTable } from './table/table'; +export const mockReadFileSync = jest.fn(); +jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); -addDocView({ - title: i18n.translate('kbnDocViews.table.tableTitle', { - defaultMessage: 'Table', - }), - order: 10, - component: DocViewTable, -}); +export const mockReadPkcs12Keystore = jest.fn(); +export const mockReadPkcs12Truststore = jest.fn(); +jest.mock('../../utils', () => ({ + readPkcs12Keystore: mockReadPkcs12Keystore, + readPkcs12Truststore: mockReadPkcs12Truststore, +})); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 1d919639abe5f..1b4fc5eafec76 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -17,7 +17,35 @@ * under the License. */ +import { + mockReadFileSync, + mockReadPkcs12Keystore, + mockReadPkcs12Truststore, +} from './elasticsearch_config.test.mocks'; + import { ElasticsearchConfig, config } from './elasticsearch_config'; +import { applyDeprecations, configDeprecationFactory } from '../config/deprecation'; + +const CONFIG_PATH = 'elasticsearch'; + +const applyElasticsearchDeprecations = (settings: Record = {}) => { + const deprecations = config.deprecations!(configDeprecationFactory); + const deprecationMessages: string[] = []; + const _config: any = {}; + _config[CONFIG_PATH] = settings; + const migrated = applyDeprecations( + _config, + deprecations.map(deprecation => ({ + deprecation, + path: CONFIG_PATH, + })), + msg => deprecationMessages.push(msg) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; test('set correct defaults', () => { const configValue = new ElasticsearchConfig(config.schema.validate({})); @@ -43,7 +71,10 @@ test('set correct defaults', () => { "sniffOnStart": false, "ssl": Object { "alwaysPresentCertificate": false, + "certificate": undefined, "certificateAuthorities": undefined, + "key": undefined, + "keyPassphrase": undefined, "verificationMode": "full", }, "username": undefined, @@ -89,23 +120,238 @@ test('#requestHeadersWhitelist accepts both string and array of strings', () => expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']); }); -test('#ssl.certificateAuthorities accepts both string and array of strings', () => { - let configValue = new ElasticsearchConfig( - config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) - ); - expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); +describe('reads files', () => { + beforeEach(() => { + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); + }); - configValue = new ElasticsearchConfig( - config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) - ); - expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); + it('reads certificate authorities when ssl.keystore.path is specified', () => { + const configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { keystore: { path: 'some-path' } } }) + ); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); - configValue = new ElasticsearchConfig( - config.schema.validate({ - ssl: { certificateAuthorities: ['some-path', 'another-path'] }, - }) - ); - expect(configValue.ssl.certificateAuthorities).toEqual(['some-path', 'another-path']); + it('reads certificate authorities when ssl.truststore.path is specified', () => { + const configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { truststore: { path: 'some-path' } } }) + ); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + }); + + it('reads certificate authorities when ssl.certificateAuthorities is specified', () => { + let configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = new ElasticsearchConfig( + config.schema.validate({ + ssl: { certificateAuthorities: ['some-path', 'another-path'] }, + }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); + + it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => { + const configValue = new ElasticsearchConfig( + config.schema.validate({ + ssl: { + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }, + }) + ); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); + + it('reads a private key and certificate when ssl.keystore.path is specified', () => { + const configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { keystore: { path: 'some-path' } } }) + ); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path.key'); + expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert'); + }); + + it('reads a private key when ssl.key is specified', () => { + const configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { key: 'some-path' } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path'); + }); + + it('reads a certificate when ssl.certificate is specified', () => { + const configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { certificate: 'some-path' } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificate).toEqual('content-of-some-path'); + }); +}); + +describe('throws when config is invalid', () => { + beforeAll(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); + }); + + it('throws if key is invalid', () => { + const value = { ssl: { key: '/invalid/key' } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); + + it('throws if certificate is invalid', () => { + const value = { ssl: { certificate: '/invalid/cert' } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/cert'"` + ); + }); + + it('throws if certificateAuthorities is invalid', () => { + const value = { ssl: { certificateAuthorities: '/invalid/ca' } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot(`"ENOENT: no such file or directory, open '/invalid/ca'"`); + }); + + it('throws if keystore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/keystore' } } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); + + it('throws if keystore does not contain a key', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({}); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot(`"Did not find key in Elasticsearch keystore."`); + }); + + it('throws if keystore does not contain a certificate', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({ key: 'foo' }); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot(`"Did not find certificate in Elasticsearch keystore."`); + }); + + it('throws if truststore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/truststore' } } }; + expect( + () => new ElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); + + it('throws if key and keystore.path are both specified', () => { + const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } }; + expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[ssl]: cannot use [key] when [keystore.path] is specified"` + ); + }); + + it('throws if certificate and keystore.path are both specified', () => { + const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } }; + expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[ssl]: cannot use [certificate] when [keystore.path] is specified"` + ); + }); +}); + +describe('deprecations', () => { + it('logs a warning if elasticsearch.username is set to "elastic"', () => { + const { messages } = applyElasticsearchDeprecations({ username: 'elastic' }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting [${CONFIG_PATH}.username] to \\"elastic\\" is deprecated. You should use the \\"kibana\\" user instead.", + ] + `); + }); + + it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => { + const { messages } = applyElasticsearchDeprecations({ username: 'otheruser' }); + expect(messages).toHaveLength(0); + }); + + it('does not log a warning if elasticsearch.username is unset', () => { + const { messages } = applyElasticsearchDeprecations({}); + expect(messages).toHaveLength(0); + }); + + it('logs a warning if ssl.key is set and ssl.certificate is not', () => { + const { messages } = applyElasticsearchDeprecations({ ssl: { key: '' } }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting [${CONFIG_PATH}.ssl.key] without [${CONFIG_PATH}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.", + ] + `); + }); + + it('logs a warning if ssl.certificate is set and ssl.key is not', () => { + const { messages } = applyElasticsearchDeprecations({ ssl: { certificate: '' } }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting [${CONFIG_PATH}.ssl.certificate] without [${CONFIG_PATH}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.", + ] + `); + }); + + it('does not log a warning if both ssl.key and ssl.certificate are set', () => { + const { messages } = applyElasticsearchDeprecations({ ssl: { key: '', certificate: '' } }); + expect(messages).toEqual([]); + }); }); test('#username throws if equal to "elastic", only while running from source', () => { diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 8d92b12ae4a77..5f06c51a53d53 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -19,55 +19,57 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Duration } from 'moment'; -import { Logger } from '../logging'; +import { readFileSync } from 'fs'; +import { ConfigDeprecationProvider } from 'src/core/server'; +import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils'; +import { ServiceConfigDescriptor } from '../internal_types'; const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); export const DEFAULT_API_VERSION = 'master'; -export type ElasticsearchConfigType = TypeOf; +export type ElasticsearchConfigType = TypeOf; type SslConfigSchema = ElasticsearchConfigType['ssl']; -export const config = { - path: 'elasticsearch', - schema: schema.object({ - sniffOnStart: schema.boolean({ defaultValue: false }), - sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { - defaultValue: false, - }), - sniffOnConnectionFault: schema.boolean({ defaultValue: false }), - hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })], { - defaultValue: 'http://localhost:9200', - }), - preserveHost: schema.boolean({ defaultValue: true }), - username: schema.maybe( - schema.conditional( - schema.contextRef('dist'), - false, - schema.string({ - validate: rawConfig => { - if (rawConfig === 'elastic') { - return ( - 'value of "elastic" is forbidden. This is a superuser account that can obfuscate ' + - 'privilege-related issues. You should use the "kibana" user instead.' - ); - } - }, - }), - schema.string() - ) - ), - password: schema.maybe(schema.string()), - requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { - defaultValue: ['authorization'], - }), - customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), - shardTimeout: schema.duration({ defaultValue: '30s' }), - requestTimeout: schema.duration({ defaultValue: '30s' }), - pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), - startupTimeout: schema.duration({ defaultValue: '5s' }), - logQueries: schema.boolean({ defaultValue: false }), - ssl: schema.object({ +const configSchema = schema.object({ + sniffOnStart: schema.boolean({ defaultValue: false }), + sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { + defaultValue: false, + }), + sniffOnConnectionFault: schema.boolean({ defaultValue: false }), + hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })], { + defaultValue: 'http://localhost:9200', + }), + preserveHost: schema.boolean({ defaultValue: true }), + username: schema.maybe( + schema.conditional( + schema.contextRef('dist'), + false, + schema.string({ + validate: rawConfig => { + if (rawConfig === 'elastic') { + return ( + 'value of "elastic" is forbidden. This is a superuser account that can obfuscate ' + + 'privilege-related issues. You should use the "kibana" user instead.' + ); + } + }, + }), + schema.string() + ) + ), + password: schema.maybe(schema.string()), + requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { + defaultValue: ['authorization'], + }), + customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), + shardTimeout: schema.duration({ defaultValue: '30s' }), + requestTimeout: schema.duration({ defaultValue: '30s' }), + pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), + startupTimeout: schema.duration({ defaultValue: '5s' }), + logQueries: schema.boolean({ defaultValue: false }), + ssl: schema.object( + { verificationMode: schema.oneOf( [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], { defaultValue: 'full' } @@ -78,12 +80,60 @@ export const config = { certificate: schema.maybe(schema.string()), key: schema.maybe(schema.string()), keyPassphrase: schema.maybe(schema.string()), + keystore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), + truststore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), alwaysPresentCertificate: schema.boolean({ defaultValue: false }), - }), - apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), - healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), - ignoreVersionMismatch: schema.boolean({ defaultValue: false }), - }), + }, + { + validate: rawConfig => { + if (rawConfig.key && rawConfig.keystore.path) { + return 'cannot use [key] when [keystore.path] is specified'; + } + if (rawConfig.certificate && rawConfig.keystore.path) { + return 'cannot use [certificate] when [keystore.path] is specified'; + } + }, + } + ), + apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), + healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), + ignoreVersionMismatch: schema.boolean({ defaultValue: false }), +}); + +const deprecations: ConfigDeprecationProvider = () => [ + (settings, fromPath, log) => { + const es = settings[fromPath]; + if (!es) { + return settings; + } + if (es.username === 'elastic') { + log( + `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana" user instead.` + ); + } + if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { + log( + `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` + ); + } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { + log( + `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` + ); + } + return settings; + }, +]; + +export const config: ServiceConfigDescriptor = { + path: 'elasticsearch', + schema: configSchema, + deprecations, }; export class ElasticsearchConfig { @@ -173,7 +223,7 @@ export class ElasticsearchConfig { */ public readonly ssl: Pick< SslConfigSchema, - Exclude + Exclude > & { certificateAuthorities?: string[] }; /** @@ -183,7 +233,7 @@ export class ElasticsearchConfig { */ public readonly customHeaders: ElasticsearchConfigType['customHeaders']; - constructor(rawConfig: ElasticsearchConfigType, log?: Logger) { + constructor(rawConfig: ElasticsearchConfigType) { this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch; this.apiVersion = rawConfig.apiVersion; this.logQueries = rawConfig.logQueries; @@ -202,24 +252,83 @@ export class ElasticsearchConfig { this.password = rawConfig.password; this.customHeaders = rawConfig.customHeaders; - const certificateAuthorities = Array.isArray(rawConfig.ssl.certificateAuthorities) - ? rawConfig.ssl.certificateAuthorities - : typeof rawConfig.ssl.certificateAuthorities === 'string' - ? [rawConfig.ssl.certificateAuthorities] - : undefined; + const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; + const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); this.ssl = { - ...rawConfig.ssl, + alwaysPresentCertificate, + key, + keyPassphrase, + certificate, certificateAuthorities, + verificationMode, }; + } +} - if (this.username === 'elastic' && log !== undefined) { - // logger is optional / not used during tests - // TODO: logger can be removed when issue #40255 is resolved to support deprecations in NP config service - log.warn( - `Setting the elasticsearch username to "elastic" is deprecated. You should use the "kibana" user instead.`, - { tags: ['deprecation'] } - ); +const readKeyAndCerts = (rawConfig: ElasticsearchConfigType) => { + let key: string | undefined; + let keyPassphrase: string | undefined; + let certificate: string | undefined; + let certificateAuthorities: string[] | undefined; + + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + certificateAuthorities = [...(certificateAuthorities || []), ...ca]; + } + }; + + if (rawConfig.ssl.keystore?.path) { + const keystore = readPkcs12Keystore( + rawConfig.ssl.keystore.path, + rawConfig.ssl.keystore.password + ); + if (!keystore.key) { + throw new Error(`Did not find key in Elasticsearch keystore.`); + } else if (!keystore.cert) { + throw new Error(`Did not find certificate in Elasticsearch keystore.`); + } + key = keystore.key; + certificate = keystore.cert; + addCAs(keystore.ca); + } else { + if (rawConfig.ssl.key) { + key = readFile(rawConfig.ssl.key); + keyPassphrase = rawConfig.ssl.keyPassphrase; + } + if (rawConfig.ssl.certificate) { + certificate = readFile(rawConfig.ssl.certificate); } } -} + + if (rawConfig.ssl.truststore?.path) { + const ca = readPkcs12Truststore( + rawConfig.ssl.truststore.path, + rawConfig.ssl.truststore.password + ); + addCAs(ca); + } + + const ca = rawConfig.ssl.certificateAuthorities; + if (ca) { + const parsed: string[] = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + addCAs(parsed); + } + } + + return { + key, + keyPassphrase, + certificate, + certificateAuthorities, + }; +}; + +const readFile = (file: string) => { + return readFileSync(file, 'utf8'); +}; diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index d935d1a66eccf..1b52f22c4da09 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -18,33 +18,67 @@ */ import { BehaviorSubject } from 'rxjs'; -import { IClusterClient } from './cluster_client'; +import { IClusterClient, ICustomClusterClient } from './cluster_client'; import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; -import { InternalElasticsearchServiceSetup } from './types'; +import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup } from './types'; const createScopedClusterClientMock = (): jest.Mocked => ({ callAsInternalUser: jest.fn(), callAsCurrentUser: jest.fn(), }); -const createClusterClientMock = (): jest.Mocked => ({ - callAsInternalUser: jest.fn(), - asScoped: jest.fn().mockImplementation(createScopedClusterClientMock), +const createCustomClusterClientMock = (): jest.Mocked => ({ + ...createClusterClientMock(), close: jest.fn(), }); +function createClusterClientMock() { + const client: jest.Mocked = { + callAsInternalUser: jest.fn(), + asScoped: jest.fn(), + }; + client.asScoped.mockReturnValue(createScopedClusterClientMock()); + return client; +} + +type MockedElasticSearchServiceSetup = jest.Mocked< + ElasticsearchServiceSetup & { + adminClient: jest.Mocked; + dataClient: jest.Mocked; + } +>; + const createSetupContractMock = () => { - const setupContract: jest.Mocked = { + const setupContract: MockedElasticSearchServiceSetup = { + createClient: jest.fn(), + adminClient: createClusterClientMock(), + dataClient: createClusterClientMock(), + }; + setupContract.createClient.mockReturnValue(createCustomClusterClientMock()); + setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + return setupContract; +}; + +type MockedInternalElasticSearchServiceSetup = jest.Mocked< + InternalElasticsearchServiceSetup & { + adminClient: jest.Mocked; + dataClient: jest.Mocked; + } +>; +const createInternalSetupContractMock = () => { + const setupContract: MockedInternalElasticSearchServiceSetup = { + ...createSetupContractMock(), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), }, - - createClient: jest.fn().mockImplementation(createClusterClientMock), - adminClient$: new BehaviorSubject((createClusterClientMock() as unknown) as IClusterClient), - dataClient$: new BehaviorSubject((createClusterClientMock() as unknown) as IClusterClient), + adminClient$: new BehaviorSubject(createClusterClientMock()), + dataClient$: new BehaviorSubject(createClusterClientMock()), }; + setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; @@ -55,14 +89,16 @@ const createMock = () => { start: jest.fn(), stop: jest.fn(), }; - mocked.setup.mockResolvedValue(createSetupContractMock()); + mocked.setup.mockResolvedValue(createInternalSetupContractMock()); mocked.stop.mockResolvedValue(); return mocked; }; export const elasticsearchServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, + createInternalSetup: createInternalSetupContractMock, + createSetup: createSetupContractMock, createClusterClient: createClusterClientMock, + createCustomClusterClient: createCustomClusterClientMock, createScopedClusterClient: createScopedClusterClientMock, }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 6c4a1f263bc71..9f694ac1c46da 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -30,6 +30,7 @@ import { loggingServiceMock } from '../logging/logging_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; +import { elasticsearchServiceMock } from './elasticsearch_service.mock'; let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); @@ -69,6 +70,27 @@ describe('#setup', () => { ); }); + it('returns data and admin client as a part of the contract', async () => { + const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce( + () => mockAdminClusterClientInstance + ).mockImplementationOnce(() => mockDataClusterClientInstance); + + const setupContract = await elasticsearchService.setup(deps); + + const adminClient = setupContract.adminClient; + const dataClient = setupContract.dataClient; + + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + await adminClient.callAsInternalUser('any'); + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + + expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + await dataClient.callAsInternalUser('any'); + expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + it('returns data and admin client observables as a part of the contract', async () => { const mockAdminClusterClientInstance = { close: jest.fn() }; const mockDataClusterClientInstance = { close: jest.fn() }; @@ -174,7 +196,11 @@ Object { undefined, ], "ssl": Object { + "alwaysPresentCertificate": undefined, + "certificate": undefined, "certificateAuthorities": undefined, + "key": undefined, + "keyPassphrase": undefined, "verificationMode": "none", }, } diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index be0a817c54146..db3fda3a504ab 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -18,17 +18,18 @@ */ import { ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { filter, first, map, publishReplay, switchMap } from 'rxjs/operators'; +import { filter, first, map, publishReplay, switchMap, take } from 'rxjs/operators'; import { CoreService } from '../../types'; import { merge } from '../../utils'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { ClusterClient } from './cluster_client'; +import { ClusterClient, ScopeableRequest } from './cluster_client'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { InternalElasticsearchServiceSetup } from './types'; +import { CallAPIOptions } from './api_types'; /** @internal */ interface CoreClusterClients { @@ -51,7 +52,7 @@ export class ElasticsearchService implements CoreService('elasticsearch') - .pipe(map(rawConfig => new ElasticsearchConfig(rawConfig, coreContext.logger.get('config')))); + .pipe(map(rawConfig => new ElasticsearchConfig(rawConfig))); } public async setup(deps: SetupDeps): Promise { @@ -94,11 +95,67 @@ export class ElasticsearchService implements CoreService clients.adminClient)); + const dataClient$ = clients$.pipe(map(clients => clients.dataClient)); + + const adminClient = { + async callAsInternalUser( + endpoint: string, + clientParams: Record = {}, + options?: CallAPIOptions + ) { + const client = await adminClient$.pipe(take(1)).toPromise(); + return await client.callAsInternalUser(endpoint, clientParams, options); + }, + asScoped(request: ScopeableRequest) { + return { + callAsInternalUser: adminClient.callAsInternalUser, + async callAsCurrentUser( + endpoint: string, + clientParams: Record = {}, + options?: CallAPIOptions + ) { + const client = await adminClient$.pipe(take(1)).toPromise(); + return await client + .asScoped(request) + .callAsCurrentUser(endpoint, clientParams, options); + }, + }; + }, + }; + const dataClient = { + async callAsInternalUser( + endpoint: string, + clientParams: Record = {}, + options?: CallAPIOptions + ) { + const client = await dataClient$.pipe(take(1)).toPromise(); + return await client.callAsInternalUser(endpoint, clientParams, options); + }, + asScoped(request: ScopeableRequest) { + return { + callAsInternalUser: dataClient.callAsInternalUser, + async callAsCurrentUser( + endpoint: string, + clientParams: Record = {}, + options?: CallAPIOptions + ) { + const client = await dataClient$.pipe(take(1)).toPromise(); + return await client + .asScoped(request) + .callAsCurrentUser(endpoint, clientParams, options); + }, + }; + }, + }; + return { legacy: { config$: clients$.pipe(map(clients => clients.config)) }, - adminClient$: clients$.pipe(map(clients => clients.adminClient)), - dataClient$: clients$.pipe(map(clients => clients.dataClient)), + adminClient$, + dataClient$, + adminClient, + dataClient, createClient: (type: string, clientConfig: Partial = {}) => { const finalConfig = merge({}, config, clientConfig); diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 1f99f86d9887b..5d64fadfaa184 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -18,7 +18,13 @@ */ export { ElasticsearchService } from './elasticsearch_service'; -export { IClusterClient, ClusterClient, FakeRequest } from './cluster_client'; +export { + ClusterClient, + FakeRequest, + IClusterClient, + ICustomClusterClient, + ScopeableRequest, +} from './cluster_client'; export { IScopedClusterClient, ScopedClusterClient, Headers } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './elasticsearch_client_config'; export { config } from './elasticsearch_config'; diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 505b57c7c9e8e..22340bf3f2fc6 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -20,7 +20,7 @@ import { Observable } from 'rxjs'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; -import { IClusterClient } from './cluster_client'; +import { IClusterClient, ICustomClusterClient } from './cluster_client'; /** * @public @@ -46,29 +46,29 @@ export interface ElasticsearchServiceSetup { readonly createClient: ( type: string, clientConfig?: Partial - ) => IClusterClient; + ) => ICustomClusterClient; /** - * Observable of clients for the `admin` cluster. Observable emits when Elasticsearch config changes on the Kibana - * server. See {@link IClusterClient}. + * A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. + * See {@link IClusterClient}. * - * @exmaple + * @example * ```js - * const client = await elasticsearch.adminClient$.pipe(take(1)).toPromise(); + * const client = core.elasticsearch.adminClient; * ``` */ - readonly adminClient$: Observable; + readonly adminClient: IClusterClient; /** - * Observable of clients for the `data` cluster. Observable emits when Elasticsearch config changes on the Kibana - * server. See {@link IClusterClient}. + * A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. + * See {@link IClusterClient}. * - * @exmaple + * @example * ```js - * const client = await elasticsearch.dataClient$.pipe(take(1)).toPromise(); + * const client = core.elasticsearch.dataClient; * ``` */ - readonly dataClient$: Observable; + readonly dataClient: IClusterClient; } /** @internal */ @@ -77,4 +77,7 @@ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceS readonly legacy: { readonly config$: Observable; }; + + readonly adminClient$: Observable; + readonly dataClient$: Observable; } diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 8856eb95ba722..28933a035c870 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -67,10 +67,12 @@ Object { ], "clientAuthentication": "none", "enabled": false, + "keystore": Object {}, "supportedProtocols": Array [ "TLSv1.1", "TLSv1.2", ], + "truststore": Object {}, }, "xsrf": Object { "disableProtection": false, @@ -87,24 +89,6 @@ exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = ` exports[`throws if invalid hostname 1`] = `"[host]: value is [asdf$%^] but it must be a valid hostname (see RFC 1123)."`; -exports[`with TLS should accept known protocols\` 1`] = ` -"[ssl.supportedProtocols.0]: types that failed validation: -- [ssl.supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500] -- [ssl.supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] -- [ssl.supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" -`; - -exports[`with TLS should accept known protocols\` 2`] = ` -"[ssl.supportedProtocols.3]: types that failed validation: -- [ssl.supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500] -- [ssl.supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] -- [ssl.supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" -`; - -exports[`with TLS throws if TLS is enabled but \`certificate\` is not specified 1`] = `"[ssl]: must specify [certificate] and [key] when ssl is enabled"`; - -exports[`with TLS throws if TLS is enabled but \`key\` is not specified 1`] = `"[ssl]: must specify [certificate] and [key] when ssl is enabled"`; - exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; exports[`with compression accepts valid referrer whitelist 1`] = ` diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 3dc5fa48bc366..7ac707b0f3d83 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -18,7 +18,7 @@ */ import uuid from 'uuid'; -import { config, HttpConfig } from '.'; +import { config } from '.'; const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; const invalidHostname = 'asdf$%^'; @@ -108,28 +108,6 @@ test('throws if xsrf.whitelist element does not start with a slash', () => { }); describe('with TLS', () => { - test('throws if TLS is enabled but `key` is not specified', () => { - const httpSchema = config.schema; - const obj = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); - }); - - test('throws if TLS is enabled but `certificate` is not specified', () => { - const httpSchema = config.schema; - const obj = { - ssl: { - enabled: true, - key: '/path/to/key', - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); - }); - test('throws if TLS is enabled but `redirectHttpFromPort` is equal to `port`', () => { const httpSchema = config.schema; const obj = { @@ -143,189 +121,16 @@ describe('with TLS', () => { }; expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); +}); - test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { - const httpSchema = config.schema; - const obj = { - port: 1234, - ssl: { - enabled: false, - clientAuthentication: 'optional', - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"[ssl]: must enable ssl to use [clientAuthentication]"` - ); - }); - - test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { - const httpSchema = config.schema; - const obj = { - port: 1234, - ssl: { - enabled: false, - clientAuthentication: 'required', - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"[ssl]: must enable ssl to use [clientAuthentication]"` - ); - }); - - test('can specify `none` for [clientAuthentication] if ssl is not enabled', () => { - const obj = { - ssl: { - enabled: false, - clientAuthentication: 'none', - }, - }; - - const configValue = config.schema.validate(obj); - expect(configValue.ssl.clientAuthentication).toBe('none'); - }); - - test('can specify single `certificateAuthority` as a string', () => { - const obj = { - ssl: { - certificate: '/path/to/certificate', - certificateAuthorities: '/authority/', - enabled: true, - key: '/path/to/key', - }, - }; - - const configValue = config.schema.validate(obj); - expect(configValue.ssl.certificateAuthorities).toBe('/authority/'); - }); - - test('can specify socket timeouts', () => { - const obj = { - keepaliveTimeout: 1e5, - socketTimeout: 5e5, - }; - const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj); - expect(keepaliveTimeout).toBe(1e5); - expect(socketTimeout).toBe(5e5); - }); - - test('can specify several `certificateAuthorities`', () => { - const obj = { - ssl: { - certificate: '/path/to/certificate', - certificateAuthorities: ['/authority/1', '/authority/2'], - enabled: true, - key: '/path/to/key', - }, - }; - - const configValue = config.schema.validate(obj); - expect(configValue.ssl.certificateAuthorities).toEqual(['/authority/1', '/authority/2']); - }); - - test('accepts known protocols`', () => { - const httpSchema = config.schema; - const singleKnownProtocol = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1'], - }, - }; - - const allKnownProtocols = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], - }, - }; - - const singleKnownProtocolConfig = httpSchema.validate(singleKnownProtocol); - expect(singleKnownProtocolConfig.ssl.supportedProtocols).toEqual(['TLSv1']); - - const allKnownProtocolsConfig = httpSchema.validate(allKnownProtocols); - expect(allKnownProtocolsConfig.ssl.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']); - }); - - test('should accept known protocols`', () => { - const httpSchema = config.schema; - - const singleUnknownProtocol = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['SOMEv100500'], - }, - }; - - const allKnownWithOneUnknownProtocols = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'], - }, - }; - - expect(() => httpSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingSnapshot(); - expect(() => - httpSchema.validate(allKnownWithOneUnknownProtocols) - ).toThrowErrorMatchingSnapshot(); - }); - - test('HttpConfig instance should properly interpret `none` client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'none', - }, - }), - {} as any - ); - - expect(httpConfig.ssl.requestCert).toBe(false); - expect(httpConfig.ssl.rejectUnauthorized).toBe(false); - }); - - test('HttpConfig instance should properly interpret `optional` client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'optional', - }, - }), - {} as any - ); - - expect(httpConfig.ssl.requestCert).toBe(true); - expect(httpConfig.ssl.rejectUnauthorized).toBe(false); - }); - - test('HttpConfig instance should properly interpret `required` client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'required', - }, - }), - {} as any - ); - - expect(httpConfig.ssl.requestCert).toBe(true); - expect(httpConfig.ssl.rejectUnauthorized).toBe(true); - }); +test('can specify socket timeouts', () => { + const obj = { + keepaliveTimeout: 1e5, + socketTimeout: 5e5, + }; + const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj); + expect(keepaliveTimeout).toBe(1e5); + expect(socketTimeout).toBe(5e5); }); describe('with compression', () => { diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index df357aeaf2731..df7b4b5af4267 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -18,11 +18,7 @@ */ import { Server } from 'http'; - -jest.mock('fs', () => ({ - readFileSync: jest.fn(), -})); - +import { readFileSync } from 'fs'; import supertest from 'supertest'; import { ByteSizeValue, schema } from '@kbn/config-schema'; @@ -39,6 +35,7 @@ import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; import { Readable } from 'stream'; import { RequestHandlerContext } from 'kibana/server'; +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; const cookieOptions = { name: 'sid', @@ -55,6 +52,14 @@ const loggingService = loggingServiceMock.create(); const logger = loggingService.get(); const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); +let certificate: string; +let key: string; + +beforeAll(() => { + certificate = readFileSync(KBN_CERT_PATH, 'utf8'); + key = readFileSync(KBN_KEY_PATH, 'utf8'); +}); + beforeEach(() => { config = { host: '127.0.0.1', @@ -68,10 +73,10 @@ beforeEach(() => { ...config, ssl: { enabled: true, - certificate: '/certificate', + certificate, cipherSuites: ['cipherSuite'], getSecureOptions: () => 0, - key: '/key', + key, redirectHttpFromPort: config.port + 1, }, } as HttpConfig; diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 22468a5b252f4..747fd5a10b168 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -17,7 +17,6 @@ * under the License. */ -import { readFileSync } from 'fs'; import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from 'hapi'; import Hoek from 'hoek'; import { ServerOptions as TLSOptions } from 'https'; @@ -66,14 +65,12 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { // TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of // `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`. const tlsOptions: TLSOptions = { - ca: - config.ssl.certificateAuthorities && - config.ssl.certificateAuthorities.map(caFilePath => readFileSync(caFilePath)), - cert: readFileSync(ssl.certificate!), + ca: ssl.certificateAuthorities, + cert: ssl.certificate, ciphers: config.ssl.cipherSuites.join(':'), // We use the server's cipher order rather than the client's to prevent the BEAST attack. honorCipherOrder: true, - key: readFileSync(ssl.key!), + key: ssl.key, passphrase: ssl.keyPassphrase, secureOptions: ssl.getSecureOptions(), requestCert: ssl.requestCert, diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index c3b9b20d84865..a1523781010d4 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -142,6 +142,61 @@ describe('Handler', () => { statusCode: 400, }); }); + + it('accept to receive an array payload', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + let body: any = null; + router.post( + { + path: '/', + validate: { + body: schema.arrayOf(schema.object({ foo: schema.string() })), + }, + }, + (context, req, res) => { + body = req.body; + return res.ok({ body: 'ok' }); + } + ); + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send([{ foo: 'bar' }, { foo: 'dolly' }]) + .expect(200); + + expect(body).toEqual([{ foo: 'bar' }, { foo: 'dolly' }]); + }); + + it('accept to receive a json primitive payload', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + let body: any = null; + router.post( + { + path: '/', + validate: { + body: schema.number(), + }, + }, + (context, req, res) => { + body = req.body; + return res.ok({ body: 'ok' }); + } + ); + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .type('json') + .send('12') + .expect(200); + + expect(body).toEqual(12); + }); }); describe('handleLegacyErrors', () => { diff --git a/src/core/server/http/router/validator/validator.test.ts b/src/core/server/http/router/validator/validator.test.ts index 729eb1b60c10a..e972e2075e705 100644 --- a/src/core/server/http/router/validator/validator.test.ts +++ b/src/core/server/http/router/validator/validator.test.ts @@ -132,4 +132,62 @@ describe('Router validator', () => { 'The validation rule provided in the handler is not valid' ); }); + + it('should validate and infer type when data is an array', () => { + expect( + RouteValidator.from({ + body: schema.arrayOf(schema.string()), + }).getBody(['foo', 'bar']) + ).toStrictEqual(['foo', 'bar']); + expect( + RouteValidator.from({ + body: schema.arrayOf(schema.number()), + }).getBody([1, 2, 3]) + ).toStrictEqual([1, 2, 3]); + expect( + RouteValidator.from({ + body: schema.arrayOf(schema.object({ foo: schema.string() })), + }).getBody([{ foo: 'bar' }, { foo: 'dolly' }]) + ).toStrictEqual([{ foo: 'bar' }, { foo: 'dolly' }]); + + expect(() => + RouteValidator.from({ + body: schema.arrayOf(schema.number()), + }).getBody(['foo', 'bar', 'dolly']) + ).toThrowError('[0]: expected value of type [number] but got [string]'); + expect(() => + RouteValidator.from({ + body: schema.arrayOf(schema.number()), + }).getBody({ foo: 'bar' }) + ).toThrowError('expected value of type [array] but got [Object]'); + }); + + it('should validate and infer type when data is a primitive', () => { + expect( + RouteValidator.from({ + body: schema.string(), + }).getBody('foobar') + ).toStrictEqual('foobar'); + expect( + RouteValidator.from({ + body: schema.number(), + }).getBody(42) + ).toStrictEqual(42); + expect( + RouteValidator.from({ + body: schema.boolean(), + }).getBody(true) + ).toStrictEqual(true); + + expect(() => + RouteValidator.from({ + body: schema.string(), + }).getBody({ foo: 'bar' }) + ).toThrowError('expected value of type [string] but got [Object]'); + expect(() => + RouteValidator.from({ + body: schema.number(), + }).getBody('foobar') + ).toThrowError('expected value of type [number] but got [string]'); + }); }); diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts index 65c0a934e6ef0..97dd2bc894f81 100644 --- a/src/core/server/http/router/validator/validator.ts +++ b/src/core/server/http/router/validator/validator.ts @@ -274,7 +274,7 @@ export class RouteValidator

{ // if options.body.output === 'stream' return schema.stream(); } else { - return schema.maybe(schema.nullable(schema.object({}, { allowUnknowns: true }))); + return schema.maybe(schema.nullable(schema.any({}))); } } } diff --git a/src/core/server/http/ssl_config.test.mocks.ts b/src/core/server/http/ssl_config.test.mocks.ts new file mode 100644 index 0000000000000..ab98c3a27920c --- /dev/null +++ b/src/core/server/http/ssl_config.test.mocks.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const mockReadFileSync = jest.fn(); +jest.mock('fs', () => { + return { readFileSync: mockReadFileSync }; +}); + +export const mockReadPkcs12Keystore = jest.fn(); +export const mockReadPkcs12Truststore = jest.fn(); +jest.mock('../../utils', () => ({ + readPkcs12Keystore: mockReadPkcs12Keystore, + readPkcs12Truststore: mockReadPkcs12Truststore, +})); diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts new file mode 100644 index 0000000000000..738f86f7a69eb --- /dev/null +++ b/src/core/server/http/ssl_config.test.ts @@ -0,0 +1,363 @@ +/* + * 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 { + mockReadFileSync, + mockReadPkcs12Keystore, + mockReadPkcs12Truststore, +} from './ssl_config.test.mocks'; + +import { sslSchema, SslConfig } from './ssl_config'; + +describe('#SslConfig', () => { + const createConfig = (obj: any) => new SslConfig(sslSchema.validate(obj)); + + beforeEach(() => { + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); + }); + + describe('throws when config is invalid', () => { + beforeEach(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); + }); + + test('throws if `key` is invalid', () => { + const obj = { key: '/invalid/key', certificate: '/valid/certificate' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); + + test('throws if `certificate` is invalid', () => { + mockReadFileSync.mockImplementationOnce((path: string) => `content-of-${path}`); + const obj = { key: '/valid/key', certificate: '/invalid/certificate' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/certificate'"` + ); + }); + + test('throws if `certificateAuthorities` is invalid', () => { + const obj = { certificateAuthorities: '/invalid/ca' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/ca'"` + ); + }); + + test('throws if `keystore.path` is invalid', () => { + const obj = { keystore: { path: '/invalid/keystore' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); + + test('throws if `keystore.path` does not contain a private key', () => { + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ + key: undefined, + certificate: 'foo', + })); + const obj = { keystore: { path: 'some-path' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"Did not find private key in keystore at [keystore.path]."` + ); + }); + + test('throws if `keystore.path` does not contain a certificate', () => { + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ + key: 'foo', + certificate: undefined, + })); + const obj = { keystore: { path: 'some-path' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"Did not find certificate in keystore at [keystore.path]."` + ); + }); + + test('throws if `truststore.path` is invalid', () => { + const obj = { truststore: { path: '/invalid/truststore' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); + }); + + describe('reads files', () => { + it('reads certificate authorities when `keystore.path` is specified', () => { + const configValue = createConfig({ keystore: { path: 'some-path' } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); + + it('reads certificate authorities when `truststore.path` is specified', () => { + const configValue = createConfig({ truststore: { path: 'some-path' } }); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + }); + + it('reads certificate authorities when `certificateAuthorities` is specified', () => { + let configValue = createConfig({ certificateAuthorities: 'some-path' }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createConfig({ certificateAuthorities: ['some-path'] }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createConfig({ certificateAuthorities: ['some-path', 'another-path'] }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); + + it('reads certificate authorities when `keystore.path`, `truststore.path`, and `certificateAuthorities` are specified', () => { + const configValue = createConfig({ + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); + + it('reads a private key and certificate when `keystore.path` is specified', () => { + const configValue = createConfig({ keystore: { path: 'some-path' } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.key).toEqual('content-of-some-path.key'); + expect(configValue.certificate).toEqual('content-of-some-path.cert'); + }); + + it('reads a private key and certificate when `key` and `certificate` are specified', () => { + const configValue = createConfig({ key: 'some-path', certificate: 'another-path' }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.key).toEqual('content-of-some-path'); + expect(configValue.certificate).toEqual('content-of-another-path'); + }); + }); +}); + +describe('#sslSchema', () => { + describe('throws when config is invalid', () => { + test('throws if both `key` and `keystore.path` are specified', () => { + const obj = { + key: '/path/to/key', + keystore: { + path: 'path/to/keystore', + }, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"cannot use [key] when [keystore.path] is specified"` + ); + }); + + test('throws if both `certificate` and `keystore.path` are specified', () => { + const obj = { + certificate: '/path/to/certificate', + keystore: { + path: 'path/to/keystore', + }, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"cannot use [certificate] when [keystore.path] is specified"` + ); + }); + + test('throws if TLS is enabled but `certificate` is specified and `key` is not', () => { + const obj = { + certificate: '/path/to/certificate', + enabled: true, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); + }); + + test('throws if TLS is enabled but `key` is specified and `certificate` is not', () => { + const obj = { + enabled: true, + key: '/path/to/key', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); + }); + + test('throws if TLS is enabled but `key`, `certificate`, and `keystore.path` are not specified', () => { + const obj = { + enabled: true, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); + }); + + test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { + const obj = { + enabled: false, + clientAuthentication: 'optional', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must enable ssl to use [clientAuthentication]"` + ); + }); + + test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { + const obj = { + enabled: false, + clientAuthentication: 'required', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must enable ssl to use [clientAuthentication]"` + ); + }); + }); + + describe('#supportedProtocols', () => { + test('accepts known protocols`', () => { + const singleKnownProtocol = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1'], + }; + + const allKnownProtocols = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], + }; + + const singleKnownProtocolConfig = sslSchema.validate(singleKnownProtocol); + expect(singleKnownProtocolConfig.supportedProtocols).toEqual(['TLSv1']); + + const allKnownProtocolsConfig = sslSchema.validate(allKnownProtocols); + expect(allKnownProtocolsConfig.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']); + }); + + test('rejects unknown protocols`', () => { + const singleUnknownProtocol = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['SOMEv100500'], + }; + + const allKnownWithOneUnknownProtocols = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'], + }; + + expect(() => sslSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingInlineSnapshot(` +"[supportedProtocols.0]: types that failed validation: +- [supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500] +- [supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] +- [supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" +`); + expect(() => sslSchema.validate(allKnownWithOneUnknownProtocols)) + .toThrowErrorMatchingInlineSnapshot(` +"[supportedProtocols.3]: types that failed validation: +- [supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500] +- [supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] +- [supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" +`); + }); + }); + + describe('#clientAuthentication', () => { + test('can specify `none` client authentication when ssl is not enabled', () => { + const obj = { + enabled: false, + clientAuthentication: 'none', + }; + + const configValue = sslSchema.validate(obj); + expect(configValue.clientAuthentication).toBe('none'); + }); + + test('should properly interpret `none` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'none', + }) + ); + + expect(sslConfig.requestCert).toBe(false); + expect(sslConfig.rejectUnauthorized).toBe(false); + }); + + test('should properly interpret `optional` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'optional', + }) + ); + + expect(sslConfig.requestCert).toBe(true); + expect(sslConfig.rejectUnauthorized).toBe(false); + }); + + test('should properly interpret `required` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'required', + }) + ); + + expect(sslConfig.requestCert).toBe(true); + expect(sslConfig.rejectUnauthorized).toBe(true); + }); + }); +}); diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index 55d6ebff93ce7..0096eeb092565 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -19,6 +19,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; import crypto from 'crypto'; +import { readFileSync } from 'fs'; +import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils'; // `crypto` type definitions doesn't currently include `crypto.constants`, see // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fa5baf1733f49cf26228a4e509914572c1b74adf/types/node/v6/index.d.ts#L3412 @@ -44,6 +46,14 @@ export const sslSchema = schema.object( }), key: schema.maybe(schema.string()), keyPassphrase: schema.maybe(schema.string()), + keystore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), + truststore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), redirectHttpFromPort: schema.maybe(schema.number()), supportedProtocols: schema.arrayOf( schema.oneOf([schema.literal('TLSv1'), schema.literal('TLSv1.1'), schema.literal('TLSv1.2')]), @@ -56,8 +66,16 @@ export const sslSchema = schema.object( }, { validate: ssl => { - if (ssl.enabled && (!ssl.key || !ssl.certificate)) { - return 'must specify [certificate] and [key] when ssl is enabled'; + if (ssl.key && ssl.keystore.path) { + return 'cannot use [key] when [keystore.path] is specified'; + } + + if (ssl.certificate && ssl.keystore.path) { + return 'cannot use [certificate] when [keystore.path] is specified'; + } + + if (ssl.enabled && (!ssl.key || !ssl.certificate) && !ssl.keystore.path) { + return 'must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled'; } if (!ssl.enabled && ssl.clientAuthentication !== 'none') { @@ -88,14 +106,49 @@ export class SslConfig { constructor(config: SslConfigType) { this.enabled = config.enabled; this.redirectHttpFromPort = config.redirectHttpFromPort; - this.key = config.key; - this.certificate = config.certificate; - this.certificateAuthorities = this.initCertificateAuthorities(config.certificateAuthorities); - this.keyPassphrase = config.keyPassphrase; this.cipherSuites = config.cipherSuites; this.supportedProtocols = config.supportedProtocols; this.requestCert = config.clientAuthentication !== 'none'; this.rejectUnauthorized = config.clientAuthentication === 'required'; + + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + this.certificateAuthorities = [...(this.certificateAuthorities || []), ...ca]; + } + }; + + if (config.keystore?.path) { + const { key, cert, ca } = readPkcs12Keystore(config.keystore.path, config.keystore.password); + if (!key) { + throw new Error(`Did not find private key in keystore at [keystore.path].`); + } else if (!cert) { + throw new Error(`Did not find certificate in keystore at [keystore.path].`); + } + this.key = key; + this.certificate = cert; + addCAs(ca); + } else if (config.key && config.certificate) { + this.key = readFile(config.key); + this.keyPassphrase = config.keyPassphrase; + this.certificate = readFile(config.certificate); + } + + if (config.truststore?.path) { + const ca = readPkcs12Truststore(config.truststore.path, config.truststore.password); + addCAs(ca); + } + + const ca = config.certificateAuthorities; + if (ca) { + const parsed: string[] = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + addCAs(parsed); + } + } } /** @@ -117,12 +170,8 @@ export class SslConfig { : secureOptions | secureOption; // eslint-disable-line no-bitwise }, 0); } - - private initCertificateAuthorities(certificateAuthorities?: string[] | string) { - if (certificateAuthorities === undefined || Array.isArray(certificateAuthorities)) { - return certificateAuthorities; - } - - return [certificateAuthorities]; - } } + +const readFile = (file: string) => { + return readFileSync(file, 'utf8'); +}; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 953fa0738597c..eccf3985fc495 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -74,6 +74,7 @@ export { CspConfig, ICspConfig } from './csp'; export { ClusterClient, IClusterClient, + ICustomClusterClient, Headers, ScopedClusterClient, IScopedClusterClient, @@ -83,6 +84,7 @@ export { ElasticsearchServiceSetup, APICaller, FakeRequest, + ScopeableRequest, } from './elasticsearch'; export * from './elasticsearch/api_types'; export { diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index e17de7364ce59..cc36b90ec526d 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -249,8 +249,8 @@ export class LegacyService implements CoreService { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, elasticsearch: { - adminClient$: setupDeps.core.elasticsearch.adminClient$, - dataClient$: setupDeps.core.elasticsearch.dataClient$, + adminClient: setupDeps.core.elasticsearch.adminClient, + dataClient: setupDeps.core.elasticsearch.dataClient, createClient: setupDeps.core.elasticsearch.createClient, }, http: { diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 53849b040c413..073d380d3aa67 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -107,7 +107,7 @@ function createCoreSetupMock() { const mock: MockedKeys = { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, savedObjects: savedObjectsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, @@ -131,7 +131,7 @@ function createInternalCoreSetupMock() { const setupDeps: InternalCoreSetup = { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createSetupContract(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 6e9a7967e9eca..6d82a8d3ec6cf 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -145,8 +145,8 @@ export function createPluginSetupContext( createContextContainer: deps.context.createContextContainer, }, elasticsearch: { - adminClient$: deps.elasticsearch.adminClient$, - dataClient$: deps.elasticsearch.dataClient$, + adminClient: deps.elasticsearch.adminClient, + dataClient: deps.elasticsearch.dataClient, createClient: deps.elasticsearch.createClient, }, http: { diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 18c04af3bb641..22dfbeecbaedd 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -414,3 +414,51 @@ test('`startPlugins` only starts plugins that were setup', async () => { ] `); }); + +describe('setup', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + const plugin: PluginWrapper = createPlugin('timeout-setup'); + jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise(i => i)); + pluginsSystem.addPlugin(plugin); + mockCreatePluginSetupContext.mockImplementation(() => ({})); + + const promise = pluginsSystem.setupPlugins(setupDeps); + jest.runAllTimers(); + + await expect(promise).rejects.toMatchInlineSnapshot( + `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + ); + }); +}); + +describe('start', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + it('throws timeout error if "start" was not completed in 30 sec.', async () => { + const plugin: PluginWrapper = createPlugin('timeout-start'); + jest.spyOn(plugin, 'setup').mockResolvedValue({}); + jest.spyOn(plugin, 'start').mockImplementation(() => new Promise(i => i)); + + pluginsSystem.addPlugin(plugin); + mockCreatePluginSetupContext.mockImplementation(() => ({})); + mockCreatePluginStartContext.mockImplementation(() => ({})); + + await pluginsSystem.setupPlugins(setupDeps); + const promise = pluginsSystem.startPlugins(startDeps); + jest.runAllTimers(); + + await expect(promise).rejects.toMatchInlineSnapshot( + `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + ); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index f437b51e5b07a..dd2df7c8e01d1 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -23,7 +23,9 @@ import { PluginWrapper } from './plugin'; import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; +import { withTimeout } from '../../utils'; +const Sec = 1000; /** @internal */ export class PluginsSystem { private readonly plugins = new Map(); @@ -85,14 +87,16 @@ export class PluginsSystem { return depContracts; }, {} as Record); - contracts.set( - pluginName, - await plugin.setup( + const contract = await withTimeout({ + promise: plugin.setup( createPluginSetupContext(this.coreContext, deps, plugin), pluginDepContracts - ) - ); + ), + timeout: 30 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, + }); + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -121,13 +125,16 @@ export class PluginsSystem { return depContracts; }, {} as Record); - contracts.set( - pluginName, - await plugin.start( + const contract = await withTimeout({ + promise: plugin.start( createPluginStartContext(this.coreContext, deps, plugin), pluginDepContracts - ) - ); + ), + timeout: 30 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, + }); + + contracts.set(pluginName, contract); } return contracts; diff --git a/src/core/server/rendering/views/styles.tsx b/src/core/server/rendering/views/styles.tsx index f41627bcfe07f..dfcb4213d90f7 100644 --- a/src/core/server/rendering/views/styles.tsx +++ b/src/core/server/rendering/views/styles.tsx @@ -28,8 +28,6 @@ interface Props { } export const Styles: FunctionComponent = ({ darkMode }) => { - const themeBackground = darkMode ? '#25262e' : '#f5f7fa'; - return ( \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg new file mode 100644 index 0000000000000..5a1d6e9a52f17 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index cd3e0d2fd9f89..c09995caab669 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -68,7 +68,7 @@ export class LocalApplicationService { isUnmounted = true; }); (async () => { - const params = { element, appBasePath: '' }; + const params = { element, appBasePath: '', onAppLeave: () => undefined }; unmountHandler = isAppMountDeprecated(app.mount) ? await app.mount({ core: npStart.core }, params) : await app.mount(params); diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 5323fb2dac2d2..d62770956b88e 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -28,7 +28,8 @@ import { I18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; import appTemplate from './app.html'; import landingTemplate from './landing.html'; -import { management, SidebarNav, MANAGEMENT_BREADCRUMB } from 'ui/management'; +import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; +import { ManagementSidebarNav } from '../../../../../plugins/management/public'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory, @@ -42,6 +43,7 @@ import { EuiIcon, EuiHorizontalRule, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; const SIDENAV_ID = 'management-sidenav'; const LANDING_ID = 'management-landing'; @@ -102,7 +104,7 @@ export function updateLandingPage(version) { ); } -export function updateSidebar(items, id) { +export function updateSidebar(legacySections, id) { const node = document.getElementById(SIDENAV_ID); if (!node) { return; @@ -110,7 +112,12 @@ export function updateSidebar(items, id) { render( - + , node ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js index 0909b6947895c..f7e654fd3c76d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js @@ -29,7 +29,7 @@ import { uiModules } from 'ui/modules'; import { fatalError, toastNotifications } from 'ui/notify'; import 'ui/accessibility/kbn_ui_ace_keyboard_mode'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { isNumeric } from 'ui/utils/numeric'; +import { isNumeric } from './lib/numeric'; import { canViewInApp } from './lib/in_app_url'; import { castEsToKbnFieldTypeName } from '../../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts new file mode 100644 index 0000000000000..bb749de8dcb71 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { keysToCamelCaseShallow } from './case_conversion'; + +describe('keysToCamelCaseShallow', () => { + test("should convert all of an object's keys to camel case", () => { + const data = { + camelCase: 'camelCase', + 'kebab-case': 'kebabCase', + snake_case: 'snakeCase', + }; + + const result = keysToCamelCaseShallow(data); + + expect(result.camelCase).toBe('camelCase'); + expect(result.kebabCase).toBe('kebabCase'); + expect(result.snakeCase).toBe('snakeCase'); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts new file mode 100644 index 0000000000000..718530eb3b602 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mapKeys, camelCase } from 'lodash'; + +export function keysToCamelCaseShallow(object: Record) { + return mapKeys(object, (value, key) => camelCase(key)); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js index f982966d1e314..caf2b5f503440 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js @@ -18,7 +18,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { keysToCamelCaseShallow } from 'ui/utils/case_conversion'; +import { keysToCamelCaseShallow } from './case_conversion'; export async function findObjects(findOptions) { const response = await kfetch({ @@ -26,5 +26,6 @@ export async function findObjects(findOptions) { pathname: '/api/kibana/management/saved_objects/_find', query: findOptions, }); + return keysToCamelCaseShallow(response); } diff --git a/src/legacy/ui/public/utils/numeric.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts similarity index 86% rename from src/legacy/ui/public/utils/numeric.ts rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts index 1342498cf5dc3..c7bc6c26a378f 100644 --- a/src/legacy/ui/public/utils/numeric.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts @@ -17,8 +17,8 @@ * under the License. */ -import _ from 'lodash'; +import { isNaN } from 'lodash'; export function isNumeric(v: any): boolean { - return !_.isNaN(v) && (typeof v === 'number' || (!Array.isArray(v) && !_.isNaN(parseFloat(v)))); + return !isNaN(v) && (typeof v === 'number' || (!Array.isArray(v) && !isNaN(parseFloat(v)))); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js index 0155b10f60218..c64b332e8ebee 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js @@ -17,9 +17,10 @@ * under the License. */ -import { StringUtils } from 'ui/utils/string_utils'; import { i18n } from '@kbn/i18n'; +const upperFirst = (str = '') => str.replace(/^./, str => str.toUpperCase()); + const names = { general: i18n.translate('kbn.management.settings.categoryNames.generalLabel', { defaultMessage: 'General', @@ -51,5 +52,5 @@ const names = { }; export function getCategoryName(category) { - return category ? names[category] || StringUtils.upperFirst(category) : ''; + return category ? names[category] || upperFirst(category) : ''; } diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json index 25ce0cb58a0ca..090586a612d4f 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, diff --git a/src/legacy/core_plugins/kibana/server/tutorials/envoyproxy_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/envoyproxy_metrics/index.js new file mode 100644 index 0000000000000..4e5301149b35b --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/envoyproxy_metrics/index.js @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../../../common/tutorials/metricbeat_instructions'; + +export function envoyproxyMetricsSpecProvider(server, context) { + const moduleName = 'envoyproxy'; + return { + id: 'envoyproxyMetrics', + name: i18n.translate('kbn.server.tutorials.envoyproxyMetrics.nameTitle', { + defaultMessage: 'Envoy Proxy metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.envoyproxyMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from Envoy Proxy.', + }), + longDescription: i18n.translate('kbn.server.tutorials.envoyproxyMetrics.longDescription', { + defaultMessage: + 'The `envoyproxy` Metricbeat module fetches monitoring metrics from Envoy Proxy. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-envoyproxy.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/envoyproxy.svg', + artifacts: { + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-envoyproxy.html', + }, + }, + completionTimeMinutes: 10, + // previewImagePath: '/plugins/kibana/home/tutorial_resources/envoyproxy_metrics/screenshot.png', + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName), + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index eb06c97629b17..53ec16c1ca593 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -83,6 +83,8 @@ import { awsLogsSpecProvider } from './aws_logs'; import { activemqLogsSpecProvider } from './activemq_logs'; import { activemqMetricsSpecProvider } from './activemq_metrics'; import { azureMetricsSpecProvider } from './azure_metrics'; +import { stanMetricsSpecProvider } from './stan_metrics'; +import { envoyproxyMetricsSpecProvider } from './envoyproxy_metrics'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -154,4 +156,6 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqLogsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(azureMetricsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(stanMetricsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(envoyproxyMetricsSpecProvider); } diff --git a/src/legacy/core_plugins/kibana/server/tutorials/stan_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/stan_metrics/index.js new file mode 100644 index 0000000000000..3f5817ce2890b --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/stan_metrics/index.js @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../../../common/tutorials/metricbeat_instructions'; + +export function stanMetricsSpecProvider(context) { + const moduleName = 'stan'; + return { + id: 'stanMetrics', + name: i18n.translate('kbn.server.tutorials.stanMetrics.nameTitle', { + defaultMessage: 'STAN metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.stanMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from the STAN server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.stanMetrics.longDescription', { + defaultMessage: + 'The `stan` Metricbeat module fetches monitoring metrics from STAN. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-stan.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/stan.svg', + artifacts: { + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-stan.html', + }, + }, + completionTimeMinutes: 10, + // previewImagePath: '/plugins/kibana/home/tutorial_resources/stan_metrics/screenshot.png', + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName), + }; +} diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index a3ebda8b3df0c..15c92f4617497 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -41,7 +41,7 @@ function TileMapOptions(props: TileMapOptionsProps) { if (!stateParams.mapType) { setValue('mapType', vis.type.editorConfig.collections.mapTypes[0]); } - }, []); + }, [setValue, stateParams.mapType, vis.type.editorConfig.collections.mapTypes]); return ( <> diff --git a/src/legacy/ui/public/utils/supports.js b/src/legacy/core_plugins/tile_map/public/css_filters.js similarity index 64% rename from src/legacy/ui/public/utils/supports.js rename to src/legacy/core_plugins/tile_map/public/css_filters.js index 19c5f34d97f36..63d6a358059b3 100644 --- a/src/legacy/ui/public/utils/supports.js +++ b/src/legacy/core_plugins/tile_map/public/css_filters.js @@ -22,22 +22,21 @@ import _ from 'lodash'; /** * just a place to put feature detection checks */ -export const supports = { - cssFilters: (function() { - const e = document.createElement('img'); - const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter']; - const test = 'grayscale(1)'; - rules.forEach(function(rule) { - e.style[rule] = test; - }); +export const supportsCssFilters = (function() { + const e = document.createElement('img'); + const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter']; + const test = 'grayscale(1)'; - document.body.appendChild(e); - const styles = window.getComputedStyle(e); - const can = _(styles) - .pick(rules) - .includes(test); - document.body.removeChild(e); + rules.forEach(function(rule) { + e.style[rule] = test; + }); - return can; - })(), -}; + document.body.appendChild(e); + const styles = window.getComputedStyle(e); + const can = _(styles) + .pick(rules) + .includes(test); + document.body.removeChild(e); + + return can; +})(); diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index f2354614ac41a..f2e6469e768e7 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -20,7 +20,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { supports } from 'ui/utils/supports'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { colorSchemas } from 'ui/vislib/components/color/truncated_colormaps'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; @@ -29,6 +28,7 @@ import { createTileMapVisualization } from './tile_map_visualization'; import { Status } from '../../visualizations/public'; import { TileMapOptions } from './components/tile_map_options'; import { MapTypes } from './map_types'; +import { supportsCssFilters } from './css_filters'; export function createTileMapTypeDefinition(dependencies) { const CoordinateMapsVisualization = createTileMapVisualization(dependencies); @@ -44,7 +44,7 @@ export function createTileMapTypeDefinition(dependencies) { defaultMessage: 'Plot latitude and longitude coordinates on a map', }), visConfig: { - canDesaturate: !!supports.cssFilters, + canDesaturate: Boolean(supportsCssFilters), defaults: { colorSchema: 'Yellow to Red', mapType: 'Scaled Circle Markers', diff --git a/src/legacy/core_plugins/timelion/common/types.ts b/src/legacy/core_plugins/timelion/common/types.ts new file mode 100644 index 0000000000000..f7084948a14f7 --- /dev/null +++ b/src/legacy/core_plugins/timelion/common/types.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null'; + +interface TimelionFunctionArgsSuggestion { + name: string; + help: string; +} + +export interface TimelionFunctionArgs { + name: string; + help?: string; + multi?: boolean; + types: TimelionFunctionArgsTypes[]; + suggestions?: TimelionFunctionArgsSuggestion[]; +} + +export interface ITimelionFunction { + aliases: string[]; + args: TimelionFunctionArgs[]; + name: string; + help: string; + chainable: boolean; + extended: boolean; + isAlias: boolean; + argsByName: { + [key: string]: TimelionFunctionArgs[]; + }; +} diff --git a/src/legacy/core_plugins/timelion/public/components/_index.scss b/src/legacy/core_plugins/timelion/public/components/_index.scss new file mode 100644 index 0000000000000..f2458a367e176 --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/components/_index.scss @@ -0,0 +1 @@ +@import './timelion_expression_input'; diff --git a/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss b/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss new file mode 100644 index 0000000000000..b1c0b5514ff7a --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss @@ -0,0 +1,18 @@ +.timExpressionInput { + flex: 1 1 auto; + display: flex; + flex-direction: column; + margin-top: $euiSize; +} + +.timExpressionInput__editor { + height: 100%; + padding-top: $euiSizeS; +} + +@include euiBreakpoint('xs', 's', 'm') { + .timExpressionInput__editor { + height: $euiSize * 15; + max-height: $euiSize * 15; + } +} diff --git a/webpackShims/moment.js b/src/legacy/core_plugins/timelion/public/components/index.ts similarity index 90% rename from webpackShims/moment.js rename to src/legacy/core_plugins/timelion/public/components/index.ts index 31476d18c9562..8d7d32a3ba262 100644 --- a/webpackShims/moment.js +++ b/src/legacy/core_plugins/timelion/public/components/index.ts @@ -17,4 +17,5 @@ * under the License. */ -module.exports = require('../node_modules/moment/min/moment-with-locales.min.js'); +export * from './timelion_expression_input'; +export * from './timelion_interval'; diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx new file mode 100644 index 0000000000000..c695d09ca822b --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useCallback, useRef, useMemo } from 'react'; +import { EuiFormLabel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; + +import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public'; +import { suggest, getSuggestion } from './timelion_expression_input_helpers'; +import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; +import { getArgValueSuggestions } from '../services/arg_value_suggestions'; + +const LANGUAGE_ID = 'timelion_expression'; +monacoEditor.languages.register({ id: LANGUAGE_ID }); + +interface TimelionExpressionInputProps { + value: string; + setValue(value: string): void; +} + +function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputProps) { + const functionList = useRef([]); + const kibana = useKibana(); + const argValueSuggestions = useMemo(getArgValueSuggestions, []); + + const provideCompletionItems = useCallback( + async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => { + const text = model.getValue(); + const wordUntil = model.getWordUntilPosition(position); + const wordRange = new monacoEditor.Range( + position.lineNumber, + wordUntil.startColumn, + position.lineNumber, + wordUntil.endColumn + ); + + const suggestions = await suggest( + text, + functionList.current, + // it's important to offset the cursor position on 1 point left + // because of PEG parser starts the line with 0, but monaco with 1 + position.column - 1, + argValueSuggestions + ); + + return { + suggestions: suggestions + ? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) => + getSuggestion(s, suggestions.type, wordRange) + ) + : [], + }; + }, + [argValueSuggestions] + ); + + const provideHover = useCallback( + async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => { + const suggestions = await suggest( + model.getValue(), + functionList.current, + // it's important to offset the cursor position on 1 point left + // because of PEG parser starts the line with 0, but monaco with 1 + position.column - 1, + argValueSuggestions + ); + + return { + contents: suggestions + ? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) => ({ + value: s.help, + })) + : [], + }; + }, + [argValueSuggestions] + ); + + useEffect(() => { + if (kibana.services.http) { + kibana.services.http.get('../api/timelion/functions').then(data => { + functionList.current = data; + }); + } + }, [kibana.services.http]); + + return ( +

+ + + +
+ +
+
+ ); +} + +export { TimelionExpressionInput }; diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts new file mode 100644 index 0000000000000..fc90c276eeca2 --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts @@ -0,0 +1,287 @@ +/* + * 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 { get, startsWith } from 'lodash'; +import PEG from 'pegjs'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; + +// @ts-ignore +import grammar from 'raw-loader!../chain.peg'; + +import { i18n } from '@kbn/i18n'; +import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; +import { ArgValueSuggestions, FunctionArg, Location } from '../services/arg_value_suggestions'; + +const Parser = PEG.generate(grammar); + +export enum SUGGESTION_TYPE { + ARGUMENTS = 'arguments', + ARGUMENT_VALUE = 'argument_value', + FUNCTIONS = 'functions', +} + +function inLocation(cursorPosition: number, location: Location) { + return cursorPosition >= location.min && cursorPosition <= location.max; +} + +function getArgumentsHelp( + functionHelp: ITimelionFunction | undefined, + functionArgs: FunctionArg[] = [] +) { + if (!functionHelp) { + return []; + } + + // Do not provide 'inputSeries' as argument suggestion for chainable functions + const argsHelp = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0); + + // ignore arguments that are already provided in function declaration + const functionArgNames = functionArgs.map(arg => arg.name); + return argsHelp.filter(arg => !functionArgNames.includes(arg.name)); +} + +async function extractSuggestionsFromParsedResult( + result: ReturnType, + cursorPosition: number, + functionList: ITimelionFunction[], + argValueSuggestions: ArgValueSuggestions +) { + const activeFunc = result.functions.find(({ location }: { location: Location }) => + inLocation(cursorPosition, location) + ); + + if (!activeFunc) { + return; + } + + const functionHelp = functionList.find(({ name }) => name === activeFunc.function); + + if (!functionHelp) { + return; + } + + // return function suggestion when cursor is outside of parentheses + // location range includes '.', function name, and '('. + const openParen = activeFunc.location.min + activeFunc.function.length + 2; + if (cursorPosition < openParen) { + return { list: [functionHelp], type: SUGGESTION_TYPE.FUNCTIONS }; + } + + // return argument value suggestions when cursor is inside argument value + const activeArg = activeFunc.arguments.find((argument: FunctionArg) => { + return inLocation(cursorPosition, argument.location); + }); + if ( + activeArg && + activeArg.type === 'namedArg' && + inLocation(cursorPosition, activeArg.value.location) + ) { + const { function: functionName, arguments: functionArgs } = activeFunc; + + const { + name: argName, + value: { text: partialInput }, + } = activeArg; + + let valueSuggestions; + if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) { + valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument( + functionName, + argName, + functionArgs, + partialInput + ); + } else { + const { suggestions: staticSuggestions } = + functionHelp.args.find(arg => arg.name === activeArg.name) || {}; + valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput( + partialInput, + staticSuggestions + ); + } + return { + list: valueSuggestions, + type: SUGGESTION_TYPE.ARGUMENT_VALUE, + }; + } + + // return argument suggestions + const argsHelp = getArgumentsHelp(functionHelp, activeFunc.arguments); + const argumentSuggestions = argsHelp.filter(arg => { + if (get(activeArg, 'type') === 'namedArg') { + return startsWith(arg.name, activeArg.name); + } else if (activeArg) { + return startsWith(arg.name, activeArg.text); + } + return true; + }); + return { list: argumentSuggestions, type: SUGGESTION_TYPE.ARGUMENTS }; +} + +export async function suggest( + expression: string, + functionList: ITimelionFunction[], + cursorPosition: number, + argValueSuggestions: ArgValueSuggestions +) { + try { + const result = await Parser.parse(expression); + + return await extractSuggestionsFromParsedResult( + result, + cursorPosition, + functionList, + argValueSuggestions + ); + } catch (err) { + let message: any; + try { + // The grammar will throw an error containing a message if the expression is formatted + // correctly and is prepared to accept suggestions. If the expression is not formatted + // correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse + // attempt will throw an error. + message = JSON.parse(err.message); + } catch (e) { + // The expression isn't correctly formatted, so JSON.parse threw an error. + return; + } + + switch (message.type) { + case 'incompleteFunction': { + let list; + if (message.function) { + // The user has start typing a function name, so we'll filter the list down to only + // possible matches. + list = functionList.filter(func => startsWith(func.name, message.function)); + } else { + // The user hasn't typed anything yet, so we'll just return the entire list. + list = functionList; + } + return { list, type: SUGGESTION_TYPE.FUNCTIONS }; + } + case 'incompleteArgument': { + const { currentFunction: functionName, currentArgs: functionArgs } = message; + const functionHelp = functionList.find(func => func.name === functionName); + return { + list: getArgumentsHelp(functionHelp, functionArgs), + type: SUGGESTION_TYPE.ARGUMENTS, + }; + } + case 'incompleteArgumentValue': { + const { name: argName, currentFunction: functionName, currentArgs: functionArgs } = message; + let valueSuggestions = []; + if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) { + valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument( + functionName, + argName, + functionArgs + ); + } else { + const functionHelp = functionList.find(func => func.name === functionName); + if (functionHelp) { + const argHelp = functionHelp.args.find(arg => arg.name === argName); + if (argHelp && argHelp.suggestions) { + valueSuggestions = argHelp.suggestions; + } + } + } + return { + list: valueSuggestions, + type: SUGGESTION_TYPE.ARGUMENT_VALUE, + }; + } + } + } +} + +export function getSuggestion( + suggestion: ITimelionFunction | TimelionFunctionArgs, + type: SUGGESTION_TYPE, + range: monacoEditor.Range +): monacoEditor.languages.CompletionItem { + let kind: monacoEditor.languages.CompletionItemKind = + monacoEditor.languages.CompletionItemKind.Method; + let insertText: string = suggestion.name; + let insertTextRules: monacoEditor.languages.CompletionItem['insertTextRules']; + let detail: string = ''; + let command: monacoEditor.languages.CompletionItem['command']; + + switch (type) { + case SUGGESTION_TYPE.ARGUMENTS: + command = { + title: 'Trigger Suggestion Dialog', + id: 'editor.action.triggerSuggest', + }; + kind = monacoEditor.languages.CompletionItemKind.Property; + insertText = `${insertText}=`; + detail = `${i18n.translate( + 'timelion.expressionSuggestions.argument.description.acceptsText', + { + defaultMessage: 'Accepts', + } + )}: ${(suggestion as TimelionFunctionArgs).types}`; + + break; + case SUGGESTION_TYPE.FUNCTIONS: + command = { + title: 'Trigger Suggestion Dialog', + id: 'editor.action.triggerSuggest', + }; + kind = monacoEditor.languages.CompletionItemKind.Function; + insertText = `${insertText}($0)`; + insertTextRules = monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet; + detail = `(${ + (suggestion as ITimelionFunction).chainable + ? i18n.translate('timelion.expressionSuggestions.func.description.chainableHelpText', { + defaultMessage: 'Chainable', + }) + : i18n.translate('timelion.expressionSuggestions.func.description.dataSourceHelpText', { + defaultMessage: 'Data source', + }) + })`; + + break; + case SUGGESTION_TYPE.ARGUMENT_VALUE: + const param = suggestion.name.split(':'); + + if (param.length === 1 || param[1]) { + insertText = `${param.length === 1 ? insertText : param[1]},`; + } + + command = { + title: 'Trigger Suggestion Dialog', + id: 'editor.action.triggerSuggest', + }; + kind = monacoEditor.languages.CompletionItemKind.Property; + detail = suggestion.help || ''; + + break; + } + + return { + detail, + insertText, + insertTextRules, + kind, + label: suggestion.name, + documentation: suggestion.help, + command, + range, + }; +} diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx b/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx new file mode 100644 index 0000000000000..6294e51e54788 --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useMemo, useCallback } from 'react'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useValidation } from 'ui/vis/editors/default/controls/agg_utils'; +import { isValidEsInterval } from '../../../../core_plugins/data/common'; + +const intervalOptions = [ + { + label: i18n.translate('timelion.vis.interval.auto', { + defaultMessage: 'Auto', + }), + value: 'auto', + }, + { + label: i18n.translate('timelion.vis.interval.second', { + defaultMessage: '1 second', + }), + value: '1s', + }, + { + label: i18n.translate('timelion.vis.interval.minute', { + defaultMessage: '1 minute', + }), + value: '1m', + }, + { + label: i18n.translate('timelion.vis.interval.hour', { + defaultMessage: '1 hour', + }), + value: '1h', + }, + { + label: i18n.translate('timelion.vis.interval.day', { + defaultMessage: '1 day', + }), + value: '1d', + }, + { + label: i18n.translate('timelion.vis.interval.week', { + defaultMessage: '1 week', + }), + value: '1w', + }, + { + label: i18n.translate('timelion.vis.interval.month', { + defaultMessage: '1 month', + }), + value: '1M', + }, + { + label: i18n.translate('timelion.vis.interval.year', { + defaultMessage: '1 year', + }), + value: '1y', + }, +]; + +interface TimelionIntervalProps { + value: string; + setValue(value: string): void; + setValidity(valid: boolean): void; +} + +function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProps) { + const onCustomInterval = useCallback( + (customValue: string) => { + setValue(customValue.trim()); + }, + [setValue] + ); + + const onChange = useCallback( + (opts: Array>) => { + setValue((opts[0] && opts[0].value) || ''); + }, + [setValue] + ); + + const selectedOptions = useMemo( + () => [intervalOptions.find(op => op.value === value) || { label: value, value }], + [value] + ); + + const isValid = intervalOptions.some(int => int.value === value) || isValidEsInterval(value); + + useValidation(setValidity, isValid); + + return ( + + + + ); +} + +export { TimelionInterval }; diff --git a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js index b90f5932b5b09..231330b898edb 100644 --- a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js +++ b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js @@ -21,9 +21,15 @@ import expect from '@kbn/expect'; import PEG from 'pegjs'; import grammar from 'raw-loader!../../chain.peg'; import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers'; -import { ArgValueSuggestionsProvider } from '../timelion_expression_suggestions/arg_value_suggestions'; +import { getArgValueSuggestions } from '../../services/arg_value_suggestions'; +import { setIndexPatterns, setSavedObjectsClient } from '../../services/plugin_services'; describe('Timelion expression suggestions', () => { + setIndexPatterns({}); + setSavedObjectsClient({}); + + const argValueSuggestions = getArgValueSuggestions(); + describe('getSuggestions', () => { const func1 = { name: 'func1', @@ -44,11 +50,6 @@ describe('Timelion expression suggestions', () => { }; const functionList = [func1, myFunc2]; let Parser; - const privateStub = () => { - return {}; - }; - const indexPatternsStub = {}; - const argValueSuggestions = ArgValueSuggestionsProvider(privateStub, indexPatternsStub); // eslint-disable-line new-cap beforeEach(function() { Parser = PEG.generate(grammar); }); diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js index c0092ca49c4f3..111db0a83ffc4 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js @@ -19,12 +19,12 @@ import _ from 'lodash'; import rison from 'rison-node'; -import { keyMap } from 'ui/utils/key_map'; import { uiModules } from 'ui/modules'; import 'ui/directives/input_focus'; import 'ui/directives/paginate'; import savedObjectFinderTemplate from './saved_object_finder.html'; import { savedSheetLoader } from '../services/saved_sheets'; +import { keyMap } from 'ui/directives/key_map'; const module = uiModules.get('kibana'); diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js index 137dd6b82046d..449c0489fea25 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js @@ -52,11 +52,11 @@ import { insertAtLocation, } from './timelion_expression_input_helpers'; import { comboBoxKeyCodes } from '@elastic/eui'; -import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions'; +import { getArgValueSuggestions } from '../services/arg_value_suggestions'; const Parser = PEG.generate(grammar); -export function TimelionExpInput($http, $timeout, Private) { +export function TimelionExpInput($http, $timeout) { return { restrict: 'E', scope: { @@ -68,7 +68,7 @@ export function TimelionExpInput($http, $timeout, Private) { replace: true, template: timelionExpressionInputTemplate, link: function(scope, elem) { - const argValueSuggestions = Private(ArgValueSuggestionsProvider); + const argValueSuggestions = getArgValueSuggestions(); const expressionInput = elem.find('[data-expression-input]'); const functionReference = {}; let suggestibleFunctionLocation = {}; diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/legacy/core_plugins/timelion/public/index.scss index f6123f4052156..7ccc6c300bc40 100644 --- a/src/legacy/core_plugins/timelion/public/index.scss +++ b/src/legacy/core_plugins/timelion/public/index.scss @@ -11,5 +11,6 @@ // timChart__legend-isLoading @import './app'; +@import './components/index'; @import './directives/index'; @import './vis/index'; diff --git a/src/legacy/core_plugins/timelion/public/legacy.ts b/src/legacy/core_plugins/timelion/public/legacy.ts index d989a68d40eeb..1cf6bb65cdc02 100644 --- a/src/legacy/core_plugins/timelion/public/legacy.ts +++ b/src/legacy/core_plugins/timelion/public/legacy.ts @@ -37,4 +37,4 @@ const setupPlugins: Readonly = { const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts index 04b27c4020ce3..0bbda4bf3646f 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts +++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts @@ -35,6 +35,7 @@ const DEBOUNCE_DELAY = 50; export function timechartFn(dependencies: TimelionVisualizationDependencies) { const { $rootScope, $compile, uiSettings } = dependencies; + return function() { return { help: 'Draw a timeseries chart', diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts index ba8c25c20abea..42f0ee3ad4725 100644 --- a/src/legacy/core_plugins/timelion/public/plugin.ts +++ b/src/legacy/core_plugins/timelion/public/plugin.ts @@ -26,12 +26,14 @@ import { } from 'kibana/public'; import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public'; import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public'; +import { PluginsStart } from 'ui/new_platform/new_platform'; import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { getTimelionVisualizationConfig } from './timelion_vis_fn'; import { getTimelionVisualization } from './vis'; import { getTimeChart } from './panels/timechart/timechart'; import { Panel } from './panels/panel'; import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; +import { setIndexPatterns, setSavedObjectsClient } from './services/plugin_services'; /** @internal */ export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup { @@ -85,12 +87,15 @@ export class TimelionPlugin implements Plugin, void> { dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel); } - public start(core: CoreStart) { + public start(core: CoreStart, plugins: PluginsStart) { const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled'); if (timelionUiEnabled === false) { core.chrome.navLinks.update('timelion', { hidden: true }); } + + setIndexPatterns(plugins.data.indexPatterns); + setSavedObjectsClient(core.savedObjects.client); } public stop(): void {} diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js b/src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts similarity index 72% rename from src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js rename to src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts index e698a69401a37..8d133de51f6d9 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js +++ b/src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts @@ -17,33 +17,51 @@ * under the License. */ -import _ from 'lodash'; -import { npStart } from 'ui/new_platform'; +import { get } from 'lodash'; +import { TimelionFunctionArgs } from '../../common/types'; +import { getIndexPatterns, getSavedObjectsClient } from './plugin_services'; -export function ArgValueSuggestionsProvider() { - const { indexPatterns } = npStart.plugins.data; - const { client: savedObjectsClient } = npStart.core.savedObjects; +export interface Location { + min: number; + max: number; +} - async function getIndexPattern(functionArgs) { - const indexPatternArg = functionArgs.find(argument => { - return argument.name === 'index'; - }); +export interface FunctionArg { + function: string; + location: Location; + name: string; + text: string; + type: string; + value: { + location: Location; + text: string; + type: string; + value: string; + }; +} + +export function getArgValueSuggestions() { + const indexPatterns = getIndexPatterns(); + const savedObjectsClient = getSavedObjectsClient(); + + async function getIndexPattern(functionArgs: FunctionArg[]) { + const indexPatternArg = functionArgs.find(({ name }) => name === 'index'); if (!indexPatternArg) { // index argument not provided return; } - const indexPatternTitle = _.get(indexPatternArg, 'value.text'); + const indexPatternTitle = get(indexPatternArg, 'value.text'); - const resp = await savedObjectsClient.find({ + const { savedObjects } = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title'], search: `"${indexPatternTitle}"`, - search_fields: ['title'], + searchFields: ['title'], perPage: 10, }); - const indexPatternSavedObject = resp.savedObjects.find(savedObject => { - return savedObject.attributes.title === indexPatternTitle; - }); + const indexPatternSavedObject = savedObjects.find( + ({ attributes }) => attributes.title === indexPatternTitle + ); if (!indexPatternSavedObject) { // index argument does not match an index pattern return; @@ -52,7 +70,7 @@ export function ArgValueSuggestionsProvider() { return await indexPatterns.get(indexPatternSavedObject.id); } - function containsFieldName(partial, field) { + function containsFieldName(partial: string, field: { name: string }) { if (!partial) { return true; } @@ -63,13 +81,13 @@ export function ArgValueSuggestionsProvider() { // Could not put with function definition since functions are defined on server const customHandlers = { es: { - index: async function(partial) { + async index(partial: string) { const search = partial ? `${partial}*` : '*'; const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type'], search: `${search}`, - search_fields: ['title'], + searchFields: ['title'], perPage: 25, }); return resp.savedObjects @@ -78,7 +96,7 @@ export function ArgValueSuggestionsProvider() { return { name: savedObject.attributes.title }; }); }, - metric: async function(partial, functionArgs) { + async metric(partial: string, functionArgs: FunctionArg[]) { if (!partial || !partial.includes(':')) { return [ { name: 'avg:' }, @@ -109,7 +127,7 @@ export function ArgValueSuggestionsProvider() { return { name: `${valueSplit[0]}:${field.name}`, help: field.type }; }); }, - split: async function(partial, functionArgs) { + async split(partial: string, functionArgs: FunctionArg[]) { const indexPattern = await getIndexPattern(functionArgs); if (!indexPattern) { return []; @@ -127,7 +145,7 @@ export function ArgValueSuggestionsProvider() { return { name: field.name, help: field.type }; }); }, - timefield: async function(partial, functionArgs) { + async timefield(partial: string, functionArgs: FunctionArg[]) { const indexPattern = await getIndexPattern(functionArgs); if (!indexPattern) { return []; @@ -150,7 +168,10 @@ export function ArgValueSuggestionsProvider() { * @param {string} argName - user provided argument name * @return {boolean} true when dynamic suggestion handler provided for function argument */ - hasDynamicSuggestionsForArgument: (functionName, argName) => { + hasDynamicSuggestionsForArgument: ( + functionName: T, + argName: keyof typeof customHandlers[T] + ) => { return customHandlers[functionName] && customHandlers[functionName][argName]; }, @@ -161,12 +182,13 @@ export function ArgValueSuggestionsProvider() { * @param {string} partial - user provided argument value * @return {array} array of dynamic suggestions matching partial */ - getDynamicSuggestionsForArgument: async ( - functionName, - argName, - functionArgs, + getDynamicSuggestionsForArgument: async ( + functionName: T, + argName: keyof typeof customHandlers[T], + functionArgs: FunctionArg[], partialInput = '' ) => { + // @ts-ignore return await customHandlers[functionName][argName](partialInput, functionArgs); }, @@ -175,7 +197,10 @@ export function ArgValueSuggestionsProvider() { * @param {array} staticSuggestions - argument value suggestions * @return {array} array of static suggestions matching partial */ - getStaticSuggestionsForInput: (partialInput = '', staticSuggestions = []) => { + getStaticSuggestionsForInput: ( + partialInput = '', + staticSuggestions: TimelionFunctionArgs['suggestions'] = [] + ) => { if (partialInput) { return staticSuggestions.filter(suggestion => { return suggestion.name.includes(partialInput); @@ -186,3 +211,5 @@ export function ArgValueSuggestionsProvider() { }, }; } + +export type ArgValueSuggestions = ReturnType; diff --git a/src/legacy/ui/public/utils/sort_prefix_first.ts b/src/legacy/core_plugins/timelion/public/services/plugin_services.ts similarity index 63% rename from src/legacy/ui/public/utils/sort_prefix_first.ts rename to src/legacy/core_plugins/timelion/public/services/plugin_services.ts index 4d1a8d7f39866..5ba4ee5e47983 100644 --- a/src/legacy/ui/public/utils/sort_prefix_first.ts +++ b/src/legacy/core_plugins/timelion/public/services/plugin_services.ts @@ -17,17 +17,14 @@ * under the License. */ -import { partition } from 'lodash'; +import { IndexPatternsContract } from 'src/plugins/data/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; -export function sortPrefixFirst(array: any[], prefix?: string | number, property?: string): any[] { - if (!prefix) { - return array; - } - const lowerCasePrefix = ('' + prefix).toLowerCase(); +export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( + 'IndexPatterns' +); - const partitions = partition(array, entry => { - const value = ('' + (property ? entry[property] : entry)).toLowerCase(); - return value.startsWith(lowerCasePrefix); - }); - return [...partitions[0], ...partitions[1]]; -} +export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter< + SavedObjectsClientContract +>('SavedObjectsClient'); diff --git a/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts b/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts index 474f464a550cd..206f9f5d8368d 100644 --- a/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts +++ b/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts @@ -28,7 +28,7 @@ const name = 'timelion_vis'; interface Arguments { expression: string; - interval: any; + interval: string; } interface RenderValue { @@ -38,7 +38,7 @@ interface RenderValue { } type Context = KibanaContext | null; -type VisParams = Arguments; +export type VisParams = Arguments; type Return = Promise>; export const getTimelionVisualizationConfig = ( @@ -60,7 +60,7 @@ export const getTimelionVisualizationConfig = ( help: '', }, interval: { - types: ['string', 'null'], + types: ['string'], default: 'auto', help: '', }, diff --git a/src/legacy/core_plugins/timelion/public/vis/_index.scss b/src/legacy/core_plugins/timelion/public/vis/_index.scss index e44b6336d33c1..17a2018f7a56a 100644 --- a/src/legacy/core_plugins/timelion/public/vis/_index.scss +++ b/src/legacy/core_plugins/timelion/public/vis/_index.scss @@ -1 +1,2 @@ @import './timelion_vis'; +@import './timelion_editor'; diff --git a/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss b/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss new file mode 100644 index 0000000000000..a9331930a86ff --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss @@ -0,0 +1,15 @@ +.visEditor--timelion { + vis-options-react-wrapper, + .visEditorSidebar__options, + .visEditorSidebar__timelionOptions { + flex: 1 1 auto; + display: flex; + flex-direction: column; + } + + .visEditor__sidebar { + @include euiBreakpoint('xs', 's', 'm') { + width: 100%; + } + } +} diff --git a/src/legacy/core_plugins/timelion/public/vis/index.ts b/src/legacy/core_plugins/timelion/public/vis/index.tsx similarity index 80% rename from src/legacy/core_plugins/timelion/public/vis/index.ts rename to src/legacy/core_plugins/timelion/public/vis/index.tsx index 7b82553a24e5b..1edcb0a5ce71c 100644 --- a/src/legacy/core_plugins/timelion/public/vis/index.ts +++ b/src/legacy/core_plugins/timelion/public/vis/index.tsx @@ -17,19 +17,24 @@ * under the License. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { DefaultEditorSize } from 'ui/vis/editor_size'; +import { VisOptionsProps } from 'ui/vis/editors/default'; +import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; import { getTimelionRequestHandler } from './timelion_request_handler'; import visConfigTemplate from './timelion_vis.html'; -import editorConfigTemplate from './timelion_vis_params.html'; import { TimelionVisualizationDependencies } from '../plugin'; // @ts-ignore import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type'; +import { TimelionOptions } from './timelion_options'; +import { VisParams } from '../timelion_vis_fn'; export const TIMELION_VIS_NAME = 'timelion'; export function getTimelionVisualization(dependencies: TimelionVisualizationDependencies) { + const { http, uiSettings } = dependencies; const timelionRequestHandler = getTimelionRequestHandler(dependencies); // return the visType object, which kibana will use to display and configure new @@ -50,7 +55,11 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe template: visConfigTemplate, }, editorConfig: { - optionsTemplate: editorConfigTemplate, + optionsTemplate: (props: VisOptionsProps) => ( + + + + ), defaultSize: DefaultEditorSize.MEDIUM, }, requestHandler: timelionRequestHandler, diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx b/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx new file mode 100644 index 0000000000000..527fcc3bc6ce8 --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { VisOptionsProps } from 'ui/vis/editors/default'; +import { VisParams } from '../timelion_vis_fn'; +import { TimelionInterval, TimelionExpressionInput } from '../components'; + +function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) { + const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [ + setValue, + ]); + const setExpressionInput = useCallback( + (value: VisParams['expression']) => setValue('expression', value), + [setValue] + ); + + return ( + + + + + ); +} + +export { TimelionOptions }; diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html b/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html deleted file mode 100644 index 9f2d2094fb1f7..0000000000000 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
- -
- -
-
- -
-
- -
- - -
- -
diff --git a/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts b/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts index 6e32a4454e707..798902aa133de 100644 --- a/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts +++ b/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts @@ -17,6 +17,8 @@ * under the License. */ +import { TimelionFunctionArgs } from '../../../common/types'; + export interface TimelionFunctionInterface extends TimelionFunctionConfig { chainable: boolean; originalFn: Function; @@ -32,21 +34,6 @@ export interface TimelionFunctionConfig { args: TimelionFunctionArgs[]; } -export interface TimelionFunctionArgs { - name: string; - help?: string; - multi?: boolean; - types: TimelionFunctionArgsTypes[]; - suggestions?: TimelionFunctionArgsSuggestion[]; -} - -export type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null'; - -export interface TimelionFunctionArgsSuggestion { - name: string; - help: string; -} - // eslint-disable-next-line import/no-default-export export default class TimelionFunction { constructor(name: string, config: TimelionFunctionConfig); diff --git a/src/legacy/core_plugins/timelion/server/types.ts b/src/legacy/core_plugins/timelion/server/types.ts index e612bc14a0daa..a035d64f764f1 100644 --- a/src/legacy/core_plugins/timelion/server/types.ts +++ b/src/legacy/core_plugins/timelion/server/types.ts @@ -17,12 +17,5 @@ * under the License. */ -export { - TimelionFunctionInterface, - TimelionFunctionConfig, - TimelionFunctionArgs, - TimelionFunctionArgsSuggestion, - TimelionFunctionArgsTypes, -} from './lib/classes/timelion_function'; - +export { TimelionFunctionInterface, TimelionFunctionConfig } from './lib/classes/timelion_function'; export { TimelionRequestQuery } from './routes/run'; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx b/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx index 53a7b1caef2a4..c70b6561c3101 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx @@ -36,7 +36,7 @@ import { MarkdownVisParams } from './types'; function MarkdownOptions({ stateParams, setValue }: VisOptionsProps) { const onMarkdownUpdate = useCallback( (value: MarkdownVisParams['markdown']) => setValue('markdown', value), - [] + [setValue] ); return ( diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js index ca5ab20957281..0776dd13a9868 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js +++ b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js @@ -21,13 +21,19 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { EuiKeyboardAccessible } from '@elastic/eui'; +import { EuiKeyboardAccessible, keyCodes } from '@elastic/eui'; class MetricVisValue extends Component { onClick = () => { this.props.onFilter(this.props.metric); }; + onKeyPress = e => { + if (e.keyCode === keyCodes.ENTER) { + this.onClick(); + } + }; + render() { const { fontSize, metric, onFilter, showLabel } = this.props; const hasFilter = !!onFilter; @@ -47,6 +53,7 @@ class MetricVisValue extends Component { className={containerClassName} style={{ backgroundColor: metric.bgColor }} onClick={hasFilter ? this.onClick : null} + onKeyPress={hasFilter ? this.onKeyPress : null} tabIndex={hasFilter ? 0 : null} role={hasFilter ? 'button' : null} > diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js index 07870379265c5..83d7ca4084a20 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js @@ -74,7 +74,11 @@ export function KbnAggTable(config, RecursionHelper) { // escape each cell in each row const csvRows = rows.map(function(row) { return Object.entries(row).map(([k, v]) => { - return escape(formatted ? columns.find(c => c.id === k).formatter.convert(v) : v); + const column = columns.find(c => c.id === k); + if (formatted && column) { + return escape(column.formatter.convert(v)); + } + return escape(v); }); }); @@ -110,12 +114,16 @@ export function KbnAggTable(config, RecursionHelper) { if (typeof $scope.dimensions === 'undefined') return; - const { buckets, metrics } = $scope.dimensions; + const { buckets, metrics, splitColumn } = $scope.dimensions; $scope.formattedColumns = table.columns .map(function(col, i) { const isBucket = buckets.find(bucket => bucket.accessor === i); - const dimension = isBucket || metrics.find(metric => metric.accessor === i); + const isSplitColumn = splitColumn + ? splitColumn.find(splitColumn => splitColumn.accessor === i) + : undefined; + const dimension = + isBucket || isSplitColumn || metrics.find(metric => metric.accessor === i); if (!dimension) return; @@ -135,18 +143,15 @@ export function KbnAggTable(config, RecursionHelper) { } const isDate = - _.get(dimension, 'format.id') === 'date' || - _.get(dimension, 'format.params.id') === 'date'; - const isNumeric = - _.get(dimension, 'format.id') === 'number' || - _.get(dimension, 'format.params.id') === 'number'; + dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; let { totalFunc } = $scope; if (typeof totalFunc === 'undefined' && showPercentage) { totalFunc = 'sum'; } - if (isNumeric || isDate || totalFunc === 'count') { + if (allowsNumericalAggregations || isDate || totalFunc === 'count') { const sum = tableRows => { return _.reduce( tableRows, @@ -161,7 +166,6 @@ export function KbnAggTable(config, RecursionHelper) { }; formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { case 'sum': { if (!isDate) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js index 982aca8d3b813..d269d7c3546ec 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js @@ -79,10 +79,14 @@ export class TimeseriesVisualization extends Component { static getYAxisDomain = model => { const axisMin = get(model, 'axis_min', '').toString(); const axisMax = get(model, 'axis_max', '').toString(); + const fit = model.series + ? model.series.filter(({ hidden }) => !hidden).every(({ fill }) => fill === '0') + : model.fill === '0'; return { min: axisMin.length ? Number(axisMin) : undefined, max: axisMax.length ? Number(axisMax) : undefined, + fit, }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js b/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js index 46dab92f3b245..2dbcdc4749a66 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js @@ -17,9 +17,9 @@ * under the License. */ -import { parseInterval } from 'ui/utils/parse_interval'; import { GTE_INTERVAL_RE } from '../../common/interval_regexp'; import { i18n } from '@kbn/i18n'; +import { parseInterval } from '../../../../../plugins/data/public'; export function validateInterval(bounds, panel, maxBuckets) { const { interval } = panel; diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx index 71a88b47a8be3..3d7fda990b2ae 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx +++ b/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx @@ -34,12 +34,12 @@ function VegaActionsMenu({ formatHJson, formatJson }: VegaActionsMenuProps) { const onHJsonCLick = useCallback(() => { formatHJson(); setIsPopoverOpen(false); - }, [isPopoverOpen, formatHJson]); + }, [formatHJson]); const onJsonCLick = useCallback(() => { formatJson(); setIsPopoverOpen(false); - }, [isPopoverOpen, formatJson]); + }, [formatJson]); const closePopover = useCallback(() => setIsPopoverOpen(false), []); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index 59c6bddb64521..cc2ab133941db 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -445,6 +445,8 @@ export const buildVislibDimensions = async ( dimensions.x.params.date = true; const { esUnit, esValue } = xAgg.buckets.getInterval(); dimensions.x.params.interval = moment.duration(esValue, esUnit); + dimensions.x.params.intervalESValue = esValue; + dimensions.x.params.intervalESUnit = esUnit; dimensions.x.params.format = xAgg.buckets.getScaledDateFormat(); dimensions.x.params.bounds = xAgg.buckets.getBounds(); } else if (xAgg.type.name === 'histogram') { diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index a18cb7de5a61b..a53e8e0498c42 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -181,7 +181,7 @@ export default () => .default('localhost'), watchPrebuild: Joi.boolean().default(false), watchProxyTimeout: Joi.number().default(10 * 60000), - useBundleCache: Joi.boolean().default(Joi.ref('$prod')), + useBundleCache: Joi.boolean().default(!!process.env.CODE_COVERAGE ? true : Joi.ref('$prod')), sourceMaps: Joi.when('$prod', { is: true, then: Joi.boolean().valid(false), diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 0af6dacee59c8..8da1b3b05fa76 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -129,7 +129,7 @@ export interface KibanaCore { plugins: PluginsSetup; }; startDeps: { - core: CoreSetup; + core: CoreStart; plugins: Record; }; logger: LoggerFactory; diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 0e96189db4650..691878cf66d27 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -106,12 +106,7 @@ describe('Saved Objects Mixin', () => { newPlatform: { __internals: { elasticsearch: { - adminClient$: { - pipe: jest.fn().mockImplementation(() => ({ - toPromise: () => - Promise.resolve({ adminClient: { callAsInternalUser: mockCallCluster } }), - })), - }, + adminClient: { callAsInternalUser: mockCallCluster }, }, }, }, diff --git a/src/legacy/server/status/lib/case_conversion.test.ts b/src/legacy/server/status/lib/case_conversion.test.ts new file mode 100644 index 0000000000000..a231ee0ba4b0f --- /dev/null +++ b/src/legacy/server/status/lib/case_conversion.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { keysToSnakeCaseShallow } from './case_conversion'; + +describe('keysToSnakeCaseShallow', () => { + test("should convert all of an object's keys to snake case", () => { + const data = { + camelCase: 'camel_case', + 'kebab-case': 'kebab_case', + snake_case: 'snake_case', + }; + + const result = keysToSnakeCaseShallow(data); + + expect(result.camel_case).toBe('camel_case'); + expect(result.kebab_case).toBe('kebab_case'); + expect(result.snake_case).toBe('snake_case'); + }); +}); diff --git a/src/legacy/server/status/lib/case_conversion.ts b/src/legacy/server/status/lib/case_conversion.ts new file mode 100644 index 0000000000000..a3ae15028daeb --- /dev/null +++ b/src/legacy/server/status/lib/case_conversion.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mapKeys, snakeCase } from 'lodash'; + +export function keysToSnakeCaseShallow(object: Record) { + return mapKeys(object, (value, key) => snakeCase(key)); +} diff --git a/src/legacy/server/status/lib/metrics.js b/src/legacy/server/status/lib/metrics.js index 322d8d8bd9ab4..2631b245e72ab 100644 --- a/src/legacy/server/status/lib/metrics.js +++ b/src/legacy/server/status/lib/metrics.js @@ -20,7 +20,7 @@ import os from 'os'; import v8 from 'v8'; import { get, isObject, merge } from 'lodash'; -import { keysToSnakeCaseShallow } from '../../../utils/case_conversion'; +import { keysToSnakeCaseShallow } from './case_conversion'; import { getAllStats as cGroupStats } from './cgroup'; import { getOSInfo } from './get_os_info'; diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 98675402b43cc..747ad025ef691 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -20,6 +20,7 @@ @import './saved_objects/index'; @import './share/index'; @import './style_compile/index'; +@import '../../../plugins/management/public/components/index'; // The following are prefixed with "vis" diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js index 929aa4d5a7a9f..a8512edee658b 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js +++ b/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js @@ -104,6 +104,8 @@ describe('initXAxis', function() { it('reads the date interval param from the x agg', function() { chart.aspects.x[0].params.interval = 'P1D'; + chart.aspects.x[0].params.intervalESValue = 1; + chart.aspects.x[0].params.intervalESUnit = 'd'; chart.aspects.x[0].params.date = true; initXAxis(chart, table); expect(chart) @@ -113,6 +115,8 @@ describe('initXAxis', function() { expect(moment.isDuration(chart.ordered.interval)).to.be(true); expect(chart.ordered.interval.toISOString()).to.eql('P1D'); + expect(chart.ordered.intervalESValue).to.be(1); + expect(chart.ordered.intervalESUnit).to.be('d'); }); it('reads the numeric interval param from the x agg', function() { diff --git a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js b/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js index 531c564ea19d6..4a81486783b08 100644 --- a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js +++ b/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js @@ -28,9 +28,19 @@ export function initXAxis(chart, table) { chart.xAxisFormat = format; chart.xAxisLabel = title; - if (params.interval) { - chart.ordered = { - interval: params.date ? moment.duration(params.interval) : params.interval, - }; + const { interval, date } = params; + if (interval) { + if (date) { + const { intervalESUnit, intervalESValue } = params; + chart.ordered = { + interval: moment.duration(interval), + intervalESUnit: intervalESUnit, + intervalESValue: intervalESValue, + }; + } else { + chart.ordered = { + interval, + }; + } } } diff --git a/src/legacy/ui/public/utils/key_map.ts b/src/legacy/ui/public/directives/key_map.ts similarity index 100% rename from src/legacy/ui/public/utils/key_map.ts rename to src/legacy/ui/public/directives/key_map.ts diff --git a/src/legacy/ui/public/directives/watch_multi/watch_multi.js b/src/legacy/ui/public/directives/watch_multi/watch_multi.js index 2a2ffe24cba9c..54b5cf08a9397 100644 --- a/src/legacy/ui/public/directives/watch_multi/watch_multi.js +++ b/src/legacy/ui/public/directives/watch_multi/watch_multi.js @@ -19,7 +19,6 @@ import _ from 'lodash'; import { uiModules } from '../../modules'; -import { callEach } from '../../utils/function'; export function watchMultiDecorator($provide) { $provide.decorator('$rootScope', function($delegate) { @@ -112,7 +111,9 @@ export function watchMultiDecorator($provide) { ) ); - return _.partial(callEach, unwatchers); + return function() { + unwatchers.forEach(listener => listener()); + }; }; function normalizeExpression($scope, expr) { diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js index 942f39fc98dec..12bf5c1cce004 100644 --- a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js +++ b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js @@ -30,6 +30,8 @@ import { EuiTitle, EuiCallOut, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; const { SearchBar } = npStart.plugins.data.ui; @@ -116,7 +118,13 @@ export class TestScript extends Component { if (previewData.error) { return ( - + -

First 10 results

+

+ +

{ - return !field.name.startsWith('_'); + const isMultiField = field.subType && field.subType.multi; + return !field.name.startsWith('_') && !isMultiField && !field.scripted; }) .forEach(field => { if (fieldsByTypeMap.has(field.type)) { @@ -180,9 +194,16 @@ export class TestScript extends Component { return ( - + - Run script + } /> @@ -219,11 +243,19 @@ export class TestScript extends Component { -

Preview results

+

+ +

- Run your script to preview the first 10 results. You can also select some additional - fields to include in your results to gain more context or add a query to filter on - specific documents. +

diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts new file mode 100644 index 0000000000000..fc4ca8469382a --- /dev/null +++ b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { groupBy } from 'lodash'; +import { organizeBy } from './organize_by'; + +describe('organizeBy', () => { + test('it works', () => { + const col = [ + { + name: 'one', + roles: ['user', 'admin', 'owner'], + }, + { + name: 'two', + roles: ['user'], + }, + { + name: 'three', + roles: ['user'], + }, + { + name: 'four', + roles: ['user', 'admin'], + }, + ]; + + const resp = organizeBy(col, 'roles'); + expect(resp).toHaveProperty('user'); + expect(resp.user.length).toBe(4); + + expect(resp).toHaveProperty('admin'); + expect(resp.admin.length).toBe(2); + + expect(resp).toHaveProperty('owner'); + expect(resp.owner.length).toBe(1); + }); + + test('behaves just like groupBy in normal scenarios', () => { + const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }]; + + const orgs = organizeBy(col, 'name'); + const groups = groupBy(col, 'name'); + + expect(orgs).toEqual(groups); + }); +}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts new file mode 100644 index 0000000000000..e923767c892cd --- /dev/null +++ b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts @@ -0,0 +1,61 @@ +/* + * 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 { each, isFunction } from 'lodash'; + +/** + * Like _.groupBy, but allows specifying multiple groups for a + * single object. + * + * organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') + * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]} + * + * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') + * // Object {'1,2,3': Array[1], '1,4': Array[1]} + * + * @param {array} collection - the list of values to organize + * @param {Function} callback - either a property name, or a callback. + * @return {object} + */ +export function organizeBy(collection: object[], callback: ((obj: object) => string) | string) { + const buckets: { [key: string]: object[] } = {}; + + function add(key: string, obj: object) { + if (!buckets[key]) { + buckets[key] = []; + } + buckets[key].push(obj); + } + + each(collection, (obj: Record) => { + const keys = isFunction(callback) ? callback(obj) : obj[callback]; + + if (!Array.isArray(keys)) { + add(keys, obj); + return; + } + + let length = keys.length; + while (length-- > 0) { + add(keys[length], obj); + } + }); + + return buckets; +} diff --git a/src/legacy/ui/public/indexed_array/indexed_array.js b/src/legacy/ui/public/indexed_array/indexed_array.js index 96b37a1423be0..39c79b2f021a3 100644 --- a/src/legacy/ui/public/indexed_array/indexed_array.js +++ b/src/legacy/ui/public/indexed_array/indexed_array.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import { inflector } from './inflector'; -import { organizeBy } from '../utils/collection'; +import { organizeBy } from './helpers/organize_by'; const pathGetter = _(_.get) .rearg(1, 0) diff --git a/src/legacy/ui/public/management/components/__snapshots__/sidebar_nav.test.ts.snap b/src/legacy/ui/public/management/components/__snapshots__/sidebar_nav.test.ts.snap deleted file mode 100644 index 3364bee33a544..0000000000000 --- a/src/legacy/ui/public/management/components/__snapshots__/sidebar_nav.test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Management filters and filters and maps section objects into SidebarNav items 1`] = ` -Array [ - Object { - "data-test-subj": "activeSection", - "href": undefined, - "icon": null, - "id": "activeSection", - "isSelected": false, - "items": Array [ - Object { - "data-test-subj": "item", - "href": undefined, - "icon": null, - "id": "item", - "isSelected": false, - "name": "item", - }, - ], - "name": "activeSection", - }, -] -`; diff --git a/src/legacy/ui/public/management/components/sidebar_nav.tsx b/src/legacy/ui/public/management/components/sidebar_nav.tsx deleted file mode 100644 index cd3d85090dce0..0000000000000 --- a/src/legacy/ui/public/management/components/sidebar_nav.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EuiIcon, EuiSideNav, IconType, EuiScreenReaderOnly } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { IndexedArray } from 'ui/indexed_array'; - -interface Subsection { - disabled: boolean; - visible: boolean; - id: string; - display: string; - url?: string; - icon?: IconType; -} -interface Section extends Subsection { - visibleItems: IndexedArray; -} - -const sectionVisible = (section: Subsection) => !section.disabled && section.visible; -const sectionToNav = (selectedId: string) => ({ display, id, url, icon }: Subsection) => ({ - id, - name: display, - icon: icon ? : null, - isSelected: selectedId === id, - href: url, - 'data-test-subj': id, -}); - -export const sideNavItems = (sections: Section[], selectedId: string) => - sections - .filter(sectionVisible) - .filter(section => section.visibleItems.filter(sectionVisible).length) - .map(section => ({ - items: section.visibleItems.filter(sectionVisible).map(sectionToNav(selectedId)), - ...sectionToNav(selectedId)(section), - })); - -interface SidebarNavProps { - sections: Section[]; - selectedId: string; -} - -interface SidebarNavState { - isSideNavOpenOnMobile: boolean; -} - -export class SidebarNav extends React.Component { - constructor(props: SidebarNavProps) { - super(props); - this.state = { - isSideNavOpenOnMobile: false, - }; - } - - public render() { - const HEADER_ID = 'management-nav-header'; - - return ( - <> - -

- {i18n.translate('common.ui.management.nav.label', { - defaultMessage: 'Management', - })} -

-
- - - ); - } - - private renderMobileTitle() { - return ; - } - - private toggleOpenOnMobile = () => { - this.setState({ - isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile, - }); - }; -} diff --git a/src/legacy/ui/public/management/index.js b/src/legacy/ui/public/management/index.js index ed8ddb65315e2..b2f1946dbc59c 100644 --- a/src/legacy/ui/public/management/index.js +++ b/src/legacy/ui/public/management/index.js @@ -23,8 +23,6 @@ export { PAGE_FOOTER_COMPONENT, } from '../../../core_plugins/kibana/public/management/sections/settings/components/default_component_registry'; export { registerSettingsComponent } from '../../../core_plugins/kibana/public/management/sections/settings/components/component_registry'; -export { SidebarNav } from './components'; export { MANAGEMENT_BREADCRUMB } from './breadcrumbs'; - import { npStart } from 'ui/new_platform'; export const management = npStart.plugins.management.legacy; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 3d4292cef27f4..06424ea48a40f 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -148,6 +148,8 @@ export const npStart = { legacy: { getSection: () => ({ register: sinon.fake(), + deregister: sinon.fake(), + hasItem: sinon.fake(), }), }, }, diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index cd1af311d4eff..e050ffd5b530c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -62,6 +62,7 @@ describe('ui/new_platform', () => { expect(mountMock).toHaveBeenCalledWith({ element: elementMock[0], appBasePath: '/test/base/path/app/test', + onAppLeave: expect.any(Function), }); }); @@ -82,6 +83,7 @@ describe('ui/new_platform', () => { expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], appBasePath: '/test/base/path/app/test', + onAppLeave: expect.any(Function), }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index df3fa7c6ea466..c948565bb0835 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -124,7 +124,11 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const params = { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) }; + const params = { + element, + appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`), + onAppLeave: () => undefined, + }; const unmount = isAppMountDeprecated(app.mount) ? await app.mount({ core: npStart.core }, params) : await app.mount(params); diff --git a/src/legacy/ui/public/registry/doc_views.ts b/src/legacy/ui/public/registry/doc_views.ts deleted file mode 100644 index bf1e8416ae66d..0000000000000 --- a/src/legacy/ui/public/registry/doc_views.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { convertDirectiveToRenderFn } from './doc_views_helpers'; -import { DocView, DocViewInput, ElasticSearchHit, DocViewInputFn } from './doc_views_types'; - -export { DocViewRenderProps, DocView, DocViewRenderFn } from './doc_views_types'; - -export interface DocViewsRegistry { - docViews: DocView[]; - addDocView: (docView: DocViewInput) => void; - getDocViewsSorted: (hit: ElasticSearchHit) => DocView[]; -} - -export const docViews: DocView[] = []; - -/** - * Extends and adds the given doc view to the registry array - */ -export function addDocView(docView: DocViewInput) { - if (docView.directive) { - // convert angular directive to render function for backwards compatibility - docView.render = convertDirectiveToRenderFn(docView.directive); - } - if (typeof docView.shouldShow !== 'function') { - docView.shouldShow = () => true; - } - docViews.push(docView as DocView); -} - -/** - * Empty array of doc views for testing - */ -export function emptyDocViews() { - docViews.length = 0; -} - -/** - * Returns a sorted array of doc_views for rendering tabs - */ -export function getDocViewsSorted(hit: ElasticSearchHit): DocView[] { - return docViews - .filter(docView => docView.shouldShow(hit)) - .sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1)); -} -/** - * Provider for compatibility with 3rd Party plugins - */ -export const DocViewsRegistryProvider = { - register: (docViewRaw: DocViewInput | DocViewInputFn) => { - const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw; - addDocView(docView); - }, -}; diff --git a/src/legacy/ui/public/saved_objects/helpers/string_utils.test.ts b/src/legacy/ui/public/saved_objects/helpers/string_utils.test.ts new file mode 100644 index 0000000000000..5b108a4cc0180 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/string_utils.test.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { StringUtils } from './string_utils'; + +describe('StringUtils class', () => { + describe('static upperFirst', () => { + test('should converts the first character of string to upper case', () => { + expect(StringUtils.upperFirst()).toBe(''); + expect(StringUtils.upperFirst('')).toBe(''); + + expect(StringUtils.upperFirst('Fred')).toBe('Fred'); + expect(StringUtils.upperFirst('fred')).toBe('Fred'); + expect(StringUtils.upperFirst('FRED')).toBe('FRED'); + }); + }); +}); diff --git a/src/legacy/ui/public/utils/string_utils.ts b/src/legacy/ui/public/saved_objects/helpers/string_utils.ts similarity index 94% rename from src/legacy/ui/public/utils/string_utils.ts rename to src/legacy/ui/public/saved_objects/helpers/string_utils.ts index 22a57aeb07933..fb10b792b7e69 100644 --- a/src/legacy/ui/public/utils/string_utils.ts +++ b/src/legacy/ui/public/saved_objects/helpers/string_utils.ts @@ -23,7 +23,7 @@ export class StringUtils { * @param str {string} * @returns {string} */ - public static upperFirst(str: string): string { + public static upperFirst(str: string = ''): string { return str ? str.charAt(0).toUpperCase() + str.slice(1) : ''; } } diff --git a/src/legacy/ui/public/saved_objects/saved_object_loader.ts b/src/legacy/ui/public/saved_objects/saved_object_loader.ts index eb880ce5380c0..7784edc9ad528 100644 --- a/src/legacy/ui/public/saved_objects/saved_object_loader.ts +++ b/src/legacy/ui/public/saved_objects/saved_object_loader.ts @@ -18,7 +18,7 @@ */ import { SavedObject } from 'ui/saved_objects/types'; import { ChromeStart, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public'; -import { StringUtils } from '../utils/string_utils'; +import { StringUtils } from './helpers/string_utils'; /** * The SavedObjectLoader class provides some convenience functions diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js index 4bf386febf836..e253d49c04131 100644 --- a/src/legacy/ui/public/state_management/app_state.js +++ b/src/legacy/ui/public/state_management/app_state.js @@ -31,7 +31,6 @@ import { uiModules } from '../modules'; import { StateProvider } from './state'; import '../persisted_state'; import { createLegacyClass } from '../utils/legacy_class'; -import { callEach } from '../utils/function'; const urlParam = '_a'; @@ -62,7 +61,8 @@ export function AppStateProvider(Private, $location, $injector) { AppState.prototype.destroy = function() { AppState.Super.prototype.destroy.call(this); AppState.getAppState._set(null); - callEach(eventUnsubscribers); + + eventUnsubscribers.forEach(listener => listener()); }; /** diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index a9898303fa5be..289d4b8006cba 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -29,12 +29,11 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import angular from 'angular'; import rison from 'rison-node'; -import { applyDiff } from '../utils/diff_object'; +import { applyDiff } from './utils/diff_object'; import { EventsProvider } from '../events'; import { fatalError, toastNotifications } from '../notify'; import './config_provider'; import { createLegacyClass } from '../utils/legacy_class'; -import { callEach } from '../utils/function'; import { hashedItemStore, isStateHash, @@ -66,7 +65,7 @@ export function StateProvider( this._hashedItemStore = _hashedItemStore; // When the URL updates we need to fetch the values from the URL - this._cleanUpListeners = _.partial(callEach, [ + this._cleanUpListeners = [ // partial route update, no app reload $rootScope.$on('$routeUpdate', () => { this.fetch(); @@ -85,7 +84,7 @@ export function StateProvider( this.fetch(); } }), - ]); + ]; // Initialize the State with fetch this.fetch(); @@ -242,7 +241,9 @@ export function StateProvider( */ State.prototype.destroy = function() { this.off(); // removes all listeners - this._cleanUpListeners(); // Removes the $routeUpdate listener + + // Removes the $routeUpdate listener + this._cleanUpListeners.forEach(listener => listener(this)); }; State.prototype.setDefaults = function(defaults) { diff --git a/src/legacy/ui/public/utils/__tests__/diff_object.js b/src/legacy/ui/public/state_management/utils/diff_object.test.ts similarity index 57% rename from src/legacy/ui/public/utils/__tests__/diff_object.js rename to src/legacy/ui/public/state_management/utils/diff_object.test.ts index 8459aa60436b1..d93fa0fe5a169 100644 --- a/src/legacy/ui/public/utils/__tests__/diff_object.js +++ b/src/legacy/ui/public/state_management/utils/diff_object.test.ts @@ -17,76 +17,88 @@ * under the License. */ -import expect from '@kbn/expect'; -import _ from 'lodash'; -import { applyDiff } from '../diff_object'; +import { cloneDeep } from 'lodash'; +import { applyDiff } from './diff_object'; -describe('ui/utils/diff_object', function() { - it('should list the removed keys', function() { +describe('diff_object', () => { + test('should list the removed keys', () => { const target = { test: 'foo' }; const source = { foo: 'test' }; const results = applyDiff(target, source); - expect(results).to.have.property('removed'); - expect(results.removed).to.eql(['test']); + + expect(results).toHaveProperty('removed'); + expect(results.removed).toEqual(['test']); }); - it('should list the changed keys', function() { + test('should list the changed keys', () => { const target = { foo: 'bar' }; const source = { foo: 'test' }; const results = applyDiff(target, source); - expect(results).to.have.property('changed'); - expect(results.changed).to.eql(['foo']); + + expect(results).toHaveProperty('changed'); + expect(results.changed).toEqual(['foo']); }); - it('should list the added keys', function() { + test('should list the added keys', () => { const target = {}; const source = { foo: 'test' }; const results = applyDiff(target, source); - expect(results).to.have.property('added'); - expect(results.added).to.eql(['foo']); + + expect(results).toHaveProperty('added'); + expect(results.added).toEqual(['foo']); }); - it('should list all the keys that are change or removed', function() { + test('should list all the keys that are change or removed', () => { const target = { foo: 'bar', test: 'foo' }; const source = { foo: 'test' }; const results = applyDiff(target, source); - expect(results).to.have.property('keys'); - expect(results.keys).to.eql(['foo', 'test']); + + expect(results).toHaveProperty('keys'); + expect(results.keys).toEqual(['foo', 'test']); }); - it('should ignore functions', function() { + test('should ignore functions', () => { const target = { foo: 'bar', test: 'foo' }; - const source = { foo: 'test', fn: _.noop }; + const source = { foo: 'test', fn: () => {} }; + applyDiff(target, source); - expect(target).to.not.have.property('fn'); + + expect(target).not.toHaveProperty('fn'); }); - it('should ignore underscores', function() { + test('should ignore underscores', () => { const target = { foo: 'bar', test: 'foo' }; const source = { foo: 'test', _private: 'foo' }; + applyDiff(target, source); - expect(target).to.not.have.property('_private'); + + expect(target).not.toHaveProperty('_private'); }); - it('should ignore dollar signs', function() { + test('should ignore dollar signs', () => { const target = { foo: 'bar', test: 'foo' }; const source = { foo: 'test', $private: 'foo' }; + applyDiff(target, source); - expect(target).to.not.have.property('$private'); + + expect(target).not.toHaveProperty('$private'); }); - it('should not list any changes for similar objects', function() { + test('should not list any changes for similar objects', () => { const target = { foo: 'bar', test: 'foo' }; const source = { foo: 'bar', test: 'foo', $private: 'foo' }; const results = applyDiff(target, source); - expect(results.changed).to.be.empty(); + + expect(results.changed).toEqual([]); }); - it('should only change keys that actually changed', function() { + test('should only change keys that actually changed', () => { const obj = { message: 'foo' }; - const target = { obj: obj, message: 'foo' }; - const source = { obj: _.cloneDeep(obj), message: 'test' }; + const target = { obj, message: 'foo' }; + const source = { obj: cloneDeep(obj), message: 'test' }; + applyDiff(target, source); - expect(target.obj).to.be(obj); + + expect(target.obj).toBe(obj); }); }); diff --git a/src/legacy/ui/public/state_management/utils/diff_object.ts b/src/legacy/ui/public/state_management/utils/diff_object.ts new file mode 100644 index 0000000000000..2590e2271f771 --- /dev/null +++ b/src/legacy/ui/public/state_management/utils/diff_object.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { keys, isFunction, difference, filter, union, pick, each, assign, isEqual } from 'lodash'; + +export interface IDiffObject { + removed: string[]; + added: string[]; + changed: string[]; + keys: string[]; +} + +/** + * Filter the private vars + * @param {string} key The keys + * @returns {boolean} + */ +const filterPrivateAndMethods = function(obj: Record) { + return function(key: string) { + if (isFunction(obj[key])) return false; + if (key.charAt(0) === '$') return false; + return key.charAt(0) !== '_'; + }; +}; + +export function applyDiff(target: Record, source: Record) { + const diff: IDiffObject = { + removed: [], + added: [], + changed: [], + keys: [], + }; + + const targetKeys = keys(target).filter(filterPrivateAndMethods(target)); + const sourceKeys = keys(source).filter(filterPrivateAndMethods(source)); + + // Find the keys to be removed + diff.removed = difference(targetKeys, sourceKeys); + + // Find the keys to be added + diff.added = difference(sourceKeys, targetKeys); + + // Find the keys that will be changed + diff.changed = filter(sourceKeys, key => !isEqual(target[key], source[key])); + + // Make a list of all the keys that are changing + diff.keys = union(diff.changed, diff.removed, diff.added); + + // Remove all the keys + each(diff.removed, key => { + delete target[key]; + }); + + // Assign the changed to the source to the target + assign(target, pick(source, diff.changed)); + // Assign the added to the source to the target + assign(target, pick(source, diff.added)); + + return diff; +} diff --git a/src/legacy/ui/public/time_buckets/time_buckets.js b/src/legacy/ui/public/time_buckets/time_buckets.js index 92de88b47e3c3..96ba4bfb2ac2c 100644 --- a/src/legacy/ui/public/time_buckets/time_buckets.js +++ b/src/legacy/ui/public/time_buckets/time_buckets.js @@ -20,13 +20,12 @@ import _ from 'lodash'; import moment from 'moment'; import { npStart } from 'ui/new_platform'; -import { parseInterval } from '../utils/parse_interval'; import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; import { convertDurationToNormalizedEsInterval, convertIntervalToEsInterval, } from './calc_es_interval'; -import { FIELD_FORMAT_IDS } from '../../../../plugins/data/public'; +import { FIELD_FORMAT_IDS, parseInterval } from '../../../../plugins/data/public'; const getConfig = (...args) => npStart.core.uiSettings.get(...args); diff --git a/src/legacy/ui/public/utils/__tests__/collection.js b/src/legacy/ui/public/utils/__tests__/collection.js deleted file mode 100644 index 402f0387e53ce..0000000000000 --- a/src/legacy/ui/public/utils/__tests__/collection.js +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { groupBy } from 'lodash'; -import { move, pushAll, organizeBy } from '../collection'; - -describe('collection', () => { - describe('move', function() { - it('accepts previous from->to syntax', function() { - const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1]; - - expect(list[3]).to.be(1); - expect(list[8]).to.be(8); - - move(list, 8, 3); - - expect(list[8]).to.be(1); - expect(list[3]).to.be(8); - }); - - it('moves an object up based on a function callback', function() { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).to.be(0); - expect(list[5]).to.be(1); - expect(list[6]).to.be(0); - - move(list, 5, false, function(v) { - return v === 0; - }); - - expect(list[4]).to.be(1); - expect(list[5]).to.be(0); - expect(list[6]).to.be(0); - }); - - it('moves an object down based on a function callback', function() { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).to.be(0); - expect(list[5]).to.be(1); - expect(list[6]).to.be(0); - - move(list, 5, true, function(v) { - return v === 0; - }); - - expect(list[4]).to.be(0); - expect(list[5]).to.be(0); - expect(list[6]).to.be(1); - }); - - it('moves an object up based on a where callback', function() { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).to.have.property('v', 0); - expect(list[5]).to.have.property('v', 1); - expect(list[6]).to.have.property('v', 0); - - move(list, 5, false, { v: 0 }); - - expect(list[4]).to.have.property('v', 1); - expect(list[5]).to.have.property('v', 0); - expect(list[6]).to.have.property('v', 0); - }); - - it('moves an object down based on a where callback', function() { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).to.have.property('v', 0); - expect(list[5]).to.have.property('v', 1); - expect(list[6]).to.have.property('v', 0); - - move(list, 5, true, { v: 0 }); - - expect(list[4]).to.have.property('v', 0); - expect(list[5]).to.have.property('v', 0); - expect(list[6]).to.have.property('v', 1); - }); - - it('moves an object down based on a pluck callback', function() { - const list = [ - { id: 0, normal: true }, - { id: 1, normal: true }, - { id: 2, normal: true }, - { id: 3, normal: true }, - { id: 4, normal: true }, - { id: 5, normal: false }, - { id: 6, normal: true }, - { id: 7, normal: true }, - { id: 8, normal: true }, - { id: 9, normal: true }, - ]; - - expect(list[4]).to.have.property('id', 4); - expect(list[5]).to.have.property('id', 5); - expect(list[6]).to.have.property('id', 6); - - move(list, 5, true, 'normal'); - - expect(list[4]).to.have.property('id', 4); - expect(list[5]).to.have.property('id', 6); - expect(list[6]).to.have.property('id', 5); - }); - }); - - describe('pushAll', function() { - it('pushes an entire array into another', function() { - const a = [1, 2, 3, 4]; - const b = [5, 6, 7, 8]; - - const output = pushAll(b, a); - expect(output).to.be(a); - expect(a).to.eql([1, 2, 3, 4, 5, 6, 7, 8]); - expect(b).to.eql([5, 6, 7, 8]); - }); - }); - - describe('organizeBy', function() { - it('it works', function() { - const col = [ - { - name: 'one', - roles: ['user', 'admin', 'owner'], - }, - { - name: 'two', - roles: ['user'], - }, - { - name: 'three', - roles: ['user'], - }, - { - name: 'four', - roles: ['user', 'admin'], - }, - ]; - - const resp = organizeBy(col, 'roles'); - expect(resp).to.have.property('user'); - expect(resp.user).to.have.length(4); - - expect(resp).to.have.property('admin'); - expect(resp.admin).to.have.length(2); - - expect(resp).to.have.property('owner'); - expect(resp.owner).to.have.length(1); - }); - - it('behaves just like groupBy in normal scenarios', function() { - const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }]; - - const orgs = organizeBy(col, 'name'); - const groups = groupBy(col, 'name'); - expect(orgs).to.eql(groups); - }); - }); -}); diff --git a/src/legacy/ui/public/utils/__tests__/parse_interval.js b/src/legacy/ui/public/utils/__tests__/parse_interval.js deleted file mode 100644 index a33b0ab958072..0000000000000 --- a/src/legacy/ui/public/utils/__tests__/parse_interval.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { parseInterval } from '../parse_interval'; -import expect from '@kbn/expect'; - -describe('parseInterval', function() { - it('should correctly parse an interval containing unit and value', function() { - let duration = parseInterval('1d'); - expect(duration.as('d')).to.be(1); - - duration = parseInterval('2y'); - expect(duration.as('y')).to.be(2); - - duration = parseInterval('5M'); - expect(duration.as('M')).to.be(5); - - duration = parseInterval('5m'); - expect(duration.as('m')).to.be(5); - - duration = parseInterval('250ms'); - expect(duration.as('ms')).to.be(250); - - duration = parseInterval('100s'); - expect(duration.as('s')).to.be(100); - - duration = parseInterval('23d'); - expect(duration.as('d')).to.be(23); - - duration = parseInterval('52w'); - expect(duration.as('w')).to.be(52); - }); - - it('should correctly parse fractional intervals containing unit and value', function() { - let duration = parseInterval('1.5w'); - expect(duration.as('w')).to.be(1.5); - - duration = parseInterval('2.35y'); - expect(duration.as('y')).to.be(2.35); - }); - - it('should correctly bubble up intervals which are less than 1', function() { - let duration = parseInterval('0.5y'); - expect(duration.as('d')).to.be(183); - - duration = parseInterval('0.5d'); - expect(duration.as('h')).to.be(12); - }); - - it('should correctly parse a unit in an interval only', function() { - let duration = parseInterval('ms'); - expect(duration.as('ms')).to.be(1); - - duration = parseInterval('d'); - expect(duration.as('d')).to.be(1); - - duration = parseInterval('m'); - expect(duration.as('m')).to.be(1); - - duration = parseInterval('y'); - expect(duration.as('y')).to.be(1); - - duration = parseInterval('M'); - expect(duration.as('M')).to.be(1); - }); - - it('should return null for an invalid interval', function() { - let duration = parseInterval(''); - expect(duration).to.not.be.ok(); - - duration = parseInterval(null); - expect(duration).to.not.be.ok(); - - duration = parseInterval('234asdf'); - expect(duration).to.not.be.ok(); - }); -}); diff --git a/src/legacy/ui/public/utils/__tests__/sort_prefix_first.js b/src/legacy/ui/public/utils/__tests__/sort_prefix_first.js deleted file mode 100644 index 721de95cbd27d..0000000000000 --- a/src/legacy/ui/public/utils/__tests__/sort_prefix_first.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { sortPrefixFirst } from '../sort_prefix_first'; - -describe('sortPrefixFirst', function() { - it('should return the original unmodified array if no prefix is provided', function() { - const array = ['foo', 'bar', 'baz']; - const result = sortPrefixFirst(array); - expect(result).to.be(array); - expect(result).to.eql(['foo', 'bar', 'baz']); - }); - - it('should sort items that match the prefix first without modifying the original array', function() { - const array = ['foo', 'bar', 'baz']; - const result = sortPrefixFirst(array, 'b'); - expect(result).to.not.be(array); - expect(result).to.eql(['bar', 'baz', 'foo']); - expect(array).to.eql(['foo', 'bar', 'baz']); - }); - - it('should not modify the order of the array other than matching prefix without modifying the original array', function() { - const array = ['foo', 'bar', 'baz', 'qux', 'quux']; - const result = sortPrefixFirst(array, 'b'); - expect(result).to.not.be(array); - expect(result).to.eql(['bar', 'baz', 'foo', 'qux', 'quux']); - expect(array).to.eql(['foo', 'bar', 'baz', 'qux', 'quux']); - }); - - it('should sort objects by property if provided', function() { - const array = [ - { name: 'foo' }, - { name: 'bar' }, - { name: 'baz' }, - { name: 'qux' }, - { name: 'quux' }, - ]; - const result = sortPrefixFirst(array, 'b', 'name'); - expect(result).to.not.be(array); - expect(result).to.eql([ - { name: 'bar' }, - { name: 'baz' }, - { name: 'foo' }, - { name: 'qux' }, - { name: 'quux' }, - ]); - expect(array).to.eql([ - { name: 'foo' }, - { name: 'bar' }, - { name: 'baz' }, - { name: 'qux' }, - { name: 'quux' }, - ]); - }); - - it('should handle numbers', function() { - const array = [1, 50, 5]; - const result = sortPrefixFirst(array, 5); - expect(result).to.not.be(array); - expect(result).to.eql([50, 5, 1]); - }); - - it('should handle mixed case', function() { - const array = ['Date Histogram', 'Histogram']; - const prefix = 'histo'; - const result = sortPrefixFirst(array, prefix); - expect(result).to.not.be(array); - expect(result).to.eql(['Histogram', 'Date Histogram']); - }); -}); diff --git a/src/legacy/ui/public/utils/collection.test.ts b/src/legacy/ui/public/utils/collection.test.ts new file mode 100644 index 0000000000000..0841e3554c0d0 --- /dev/null +++ b/src/legacy/ui/public/utils/collection.test.ts @@ -0,0 +1,141 @@ +/* + * 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 { move } from './collection'; + +describe('collection', () => { + describe('move', () => { + test('accepts previous from->to syntax', () => { + const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1]; + + expect(list[3]).toBe(1); + expect(list[8]).toBe(8); + + move(list, 8, 3); + + expect(list[8]).toBe(1); + expect(list[3]).toBe(8); + }); + + test('moves an object up based on a function callback', () => { + const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; + + expect(list[4]).toBe(0); + expect(list[5]).toBe(1); + expect(list[6]).toBe(0); + + move(list, 5, false, (v: any) => v === 0); + + expect(list[4]).toBe(1); + expect(list[5]).toBe(0); + expect(list[6]).toBe(0); + }); + + test('moves an object down based on a function callback', () => { + const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; + + expect(list[4]).toBe(0); + expect(list[5]).toBe(1); + expect(list[6]).toBe(0); + + move(list, 5, true, (v: any) => v === 0); + + expect(list[4]).toBe(0); + expect(list[5]).toBe(0); + expect(list[6]).toBe(1); + }); + + test('moves an object up based on a where callback', () => { + const list = [ + { v: 1 }, + { v: 1 }, + { v: 1 }, + { v: 1 }, + { v: 0 }, + { v: 1 }, + { v: 0 }, + { v: 1 }, + { v: 1 }, + { v: 1 }, + { v: 1 }, + ]; + + expect(list[4]).toHaveProperty('v', 0); + expect(list[5]).toHaveProperty('v', 1); + expect(list[6]).toHaveProperty('v', 0); + + move(list, 5, false, { v: 0 }); + + expect(list[4]).toHaveProperty('v', 1); + expect(list[5]).toHaveProperty('v', 0); + expect(list[6]).toHaveProperty('v', 0); + }); + + test('moves an object down based on a where callback', () => { + const list = [ + { v: 1 }, + { v: 1 }, + { v: 1 }, + { v: 1 }, + { v: 0 }, + { v: 1 }, + { v: 0 }, + { v: 1 }, + { v: 1 }, + { v: 1 }, + { v: 1 }, + ]; + + expect(list[4]).toHaveProperty('v', 0); + expect(list[5]).toHaveProperty('v', 1); + expect(list[6]).toHaveProperty('v', 0); + + move(list, 5, true, { v: 0 }); + + expect(list[4]).toHaveProperty('v', 0); + expect(list[5]).toHaveProperty('v', 0); + expect(list[6]).toHaveProperty('v', 1); + }); + + test('moves an object down based on a pluck callback', () => { + const list = [ + { id: 0, normal: true }, + { id: 1, normal: true }, + { id: 2, normal: true }, + { id: 3, normal: true }, + { id: 4, normal: true }, + { id: 5, normal: false }, + { id: 6, normal: true }, + { id: 7, normal: true }, + { id: 8, normal: true }, + { id: 9, normal: true }, + ]; + + expect(list[4]).toHaveProperty('id', 4); + expect(list[5]).toHaveProperty('id', 5); + expect(list[6]).toHaveProperty('id', 6); + + move(list, 5, true, 'normal'); + + expect(list[4]).toHaveProperty('id', 4); + expect(list[5]).toHaveProperty('id', 6); + expect(list[6]).toHaveProperty('id', 5); + }); + }); +}); diff --git a/src/legacy/ui/public/utils/collection.ts b/src/legacy/ui/public/utils/collection.ts index 61a7388575c93..45e5a0704c37b 100644 --- a/src/legacy/ui/public/utils/collection.ts +++ b/src/legacy/ui/public/utils/collection.ts @@ -33,10 +33,10 @@ import _ from 'lodash'; * @return {array} - the objs argument */ export function move( - objs: object[], + objs: any[], obj: object | number, below: number | boolean, - qualifier: (object: object, index: number) => any + qualifier?: ((object: object, index: number) => any) | Record | string ): object[] { const origI = _.isNumber(obj) ? obj : objs.indexOf(obj); if (origI === -1) { @@ -50,7 +50,7 @@ export function move( } below = !!below; - qualifier = _.callback(qualifier); + qualifier = qualifier && _.callback(qualifier); const above = !below; const finder = below ? _.findIndex : _.findLastIndex; @@ -63,7 +63,7 @@ export function move( if (above && otherI >= origI) { return; } - return !!qualifier(otherAgg, otherI); + return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI)); }); if (targetI === -1) { @@ -74,68 +74,3 @@ export function move( objs.splice(targetI, 0, objs.splice(origI, 1)[0]); return objs; } - -/** - * Like _.groupBy, but allows specifying multiple groups for a - * single object. - * - * organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]} - * - * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {'1,2,3': Array[1], '1,4': Array[1]} - * - * @param {array} collection - the list of values to organize - * @param {Function} callback - either a property name, or a callback. - * @return {object} - */ -export function organizeBy(collection: object[], callback: (obj: object) => string | string) { - const buckets: { [key: string]: object[] } = {}; - const prop = typeof callback === 'function' ? false : callback; - - function add(key: string, obj: object) { - if (!buckets[key]) { - buckets[key] = []; - } - buckets[key].push(obj); - } - - _.each(collection, (obj: object) => { - const keys = prop === false ? callback(obj) : obj[prop]; - - if (!Array.isArray(keys)) { - add(keys, obj); - return; - } - - let length = keys.length; - while (length-- > 0) { - add(keys[length], obj); - } - }); - - return buckets; -} - -/** - * Efficient and safe version of [].push(dest, source); - * - * @param {Array} source - the array to pull values from - * @param {Array} dest - the array to push values into - * @return {Array} dest - */ -export function pushAll(source: any[], dest: any[]): any[] { - const start = dest.length; - const adding = source.length; - - // allocate - http://goo.gl/e2i0S0 - dest.length = start + adding; - - // fill sparse positions - let i = -1; - while (++i < adding) { - dest[start + i] = source[i]; - } - - return dest; -} diff --git a/src/legacy/ui/public/utils/diff_object.js b/src/legacy/ui/public/utils/diff_object.js deleted file mode 100644 index ddad5e0ae42a0..0000000000000 --- a/src/legacy/ui/public/utils/diff_object.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import angular from 'angular'; - -export function applyDiff(target, source) { - const diff = {}; - - /** - * Filter the private vars - * @param {string} key The keys - * @returns {boolean} - */ - const filterPrivateAndMethods = function(obj) { - return function(key) { - if (_.isFunction(obj[key])) return false; - if (key.charAt(0) === '$') return false; - return key.charAt(0) !== '_'; - }; - }; - - const targetKeys = _.keys(target).filter(filterPrivateAndMethods(target)); - const sourceKeys = _.keys(source).filter(filterPrivateAndMethods(source)); - - // Find the keys to be removed - diff.removed = _.difference(targetKeys, sourceKeys); - - // Find the keys to be added - diff.added = _.difference(sourceKeys, targetKeys); - - // Find the keys that will be changed - diff.changed = _.filter(sourceKeys, function(key) { - return !angular.equals(target[key], source[key]); - }); - - // Make a list of all the keys that are changing - diff.keys = _.union(diff.changed, diff.removed, diff.added); - - // Remove all the keys - _.each(diff.removed, function(key) { - delete target[key]; - }); - - // Assign the changed to the source to the target - _.assign(target, _.pick(source, diff.changed)); - // Assign the added to the source to the target - _.assign(target, _.pick(source, diff.added)); - - return diff; -} diff --git a/src/legacy/ui/public/utils/math.test.ts b/src/legacy/ui/public/utils/math.test.ts deleted file mode 100644 index 13f090e77647b..0000000000000 --- a/src/legacy/ui/public/utils/math.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { greatestCommonDivisor, leastCommonMultiple } from './math'; - -describe('math utils', () => { - describe('greatestCommonDivisor', () => { - const tests: Array<[number, number, number]> = [ - [3, 5, 1], - [30, 36, 6], - [5, 1, 1], - [9, 9, 9], - [40, 20, 20], - [3, 0, 3], - [0, 5, 5], - [0, 0, 0], - [-9, -3, 3], - [-24, 8, 8], - [22, -7, 1], - ]; - - tests.map(([a, b, expected]) => { - it(`should return ${expected} for greatestCommonDivisor(${a}, ${b})`, () => { - expect(greatestCommonDivisor(a, b)).toBe(expected); - }); - }); - }); - - describe('leastCommonMultiple', () => { - const tests: Array<[number, number, number]> = [ - [3, 5, 15], - [1, 1, 1], - [5, 6, 30], - [3, 9, 9], - [8, 20, 40], - [5, 5, 5], - [0, 5, 0], - [-4, -5, 20], - [-2, -3, 6], - [-8, 2, 8], - [-8, 5, 40], - ]; - - tests.map(([a, b, expected]) => { - it(`should return ${expected} for leastCommonMultiple(${a}, ${b})`, () => { - expect(leastCommonMultiple(a, b)).toBe(expected); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts b/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts index 31fc0d90be101..c7fb937b97424 100644 --- a/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts +++ b/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts @@ -21,7 +21,7 @@ import { TimeIntervalParam } from 'ui/vis/editors/config/types'; import { AggConfig } from '../..'; import { AggType } from '../../../agg_types'; import { IndexPattern } from '../../../../../../plugins/data/public'; -import { leastCommonMultiple } from '../../../utils/math'; +import { leastCommonMultiple } from '../../lib/least_common_multiple'; import { parseEsInterval } from '../../../../../core_plugins/data/public'; import { leastCommonInterval } from '../../lib/least_common_interval'; import { EditorConfig, EditorParamConfig, FixedParam, NumericIntervalParam } from './types'; diff --git a/src/legacy/ui/public/vis/lib/least_common_interval.ts b/src/legacy/ui/public/vis/lib/least_common_interval.ts index dfdb099249228..244bc1d0111e3 100644 --- a/src/legacy/ui/public/vis/lib/least_common_interval.ts +++ b/src/legacy/ui/public/vis/lib/least_common_interval.ts @@ -18,7 +18,7 @@ */ import dateMath from '@elastic/datemath'; -import { leastCommonMultiple } from '../../utils/math'; +import { leastCommonMultiple } from './least_common_multiple'; import { parseEsInterval } from '../../../../core_plugins/data/public'; /** diff --git a/src/legacy/ui/public/utils/case_conversion.ts b/src/legacy/ui/public/vis/lib/least_common_multiple.test.ts similarity index 60% rename from src/legacy/ui/public/utils/case_conversion.ts rename to src/legacy/ui/public/vis/lib/least_common_multiple.test.ts index e0c4cad6b4e94..d07ac96b5f4d5 100644 --- a/src/legacy/ui/public/utils/case_conversion.ts +++ b/src/legacy/ui/public/vis/lib/least_common_multiple.test.ts @@ -17,20 +17,26 @@ * under the License. */ -// TODO: This file is copied from src/legacy/utils/case_conversion.ts -// because TS-imports from utils in ui are currently not possible. -// When the build process is updated, this file can be removed +import { leastCommonMultiple } from './least_common_multiple'; -import _ from 'lodash'; +describe('leastCommonMultiple', () => { + const tests: Array<[number, number, number]> = [ + [3, 5, 15], + [1, 1, 1], + [5, 6, 30], + [3, 9, 9], + [8, 20, 40], + [5, 5, 5], + [0, 5, 0], + [-4, -5, 20], + [-2, -3, 6], + [-8, 2, 8], + [-8, 5, 40], + ]; -export function keysToSnakeCaseShallow(object: Record) { - return _.mapKeys(object, (value, key) => { - return _.snakeCase(key); + tests.map(([a, b, expected]) => { + test(`should return ${expected} for leastCommonMultiple(${a}, ${b})`, () => { + expect(leastCommonMultiple(a, b)).toBe(expected); + }); }); -} - -export function keysToCamelCaseShallow(object: Record) { - return _.mapKeys(object, (value, key) => { - return _.camelCase(key); - }); -} +}); diff --git a/src/legacy/ui/public/utils/math.ts b/src/legacy/ui/public/vis/lib/least_common_multiple.ts similarity index 95% rename from src/legacy/ui/public/utils/math.ts rename to src/legacy/ui/public/vis/lib/least_common_multiple.ts index e0cab4236e2b8..dedddbf22ab44 100644 --- a/src/legacy/ui/public/utils/math.ts +++ b/src/legacy/ui/public/vis/lib/least_common_multiple.ts @@ -24,8 +24,10 @@ * This method does not properly work for fractional (non integer) numbers. If you * pass in fractional numbers there usually will be an output, but that's not necessarily * the greatest common divisor of those two numbers. + * + * @private */ -export function greatestCommonDivisor(a: number, b: number): number { +function greatestCommonDivisor(a: number, b: number): number { return a === 0 ? Math.abs(b) : greatestCommonDivisor(b % a, a); } @@ -36,6 +38,8 @@ export function greatestCommonDivisor(a: number, b: number): number { * Since this calculation suffers from rounding issues in decimal values, this method * won't work for passing in fractional (non integer) numbers. It will return a value, * but that value won't necessarily be the mathematical correct least common multiple. + * + * @internal */ export function leastCommonMultiple(a: number, b: number): number { return Math.abs((a * b) / greatestCommonDivisor(a, b)); diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index d8a55935b705a..85b6de26b9516 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -14,6 +14,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { window.onload = function () { var files = [ '{{dllBundlePath}}/vendors.bundle.dll.js', + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', '{{regularBundlePath}}/commons.bundle.js', '{{regularBundlePath}}/{{appId}}.bundle.js' ]; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 0b266b8b62726..a935270d23fce 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -21,6 +21,7 @@ import { createHash } from 'crypto'; import Boom from 'boom'; import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { AppBootstrap } from './bootstrap'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { fromRoot } from '../../../core/server/utils'; @@ -41,18 +42,10 @@ export function uiRenderMixin(kbnServer, server, config) { // render all views from ./views server.setupViews(resolve(__dirname, 'views')); - server.exposeStaticDir( - '/node_modules/@elastic/eui/dist/{path*}', - fromRoot('node_modules/@elastic/eui/dist') - ); server.exposeStaticDir( '/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist') ); - server.exposeStaticDir( - '/node_modules/@elastic/charts/dist/{path*}', - fromRoot('node_modules/@elastic/charts/dist') - ); const translationsCache = { translations: null, hash: null }; server.route({ @@ -114,14 +107,12 @@ export function uiRenderMixin(kbnServer, server, config) { `${dllBundlePath}/vendors.style.dll.css`, ...(darkMode ? [ - `${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`, + `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, - `${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`, ] : [ - `${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`, + `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, - `${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`, ]), `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, @@ -142,6 +133,7 @@ export function uiRenderMixin(kbnServer, server, config) { regularBundlePath, dllBundlePath, styleSheetPaths, + sharedDepsFilename: UiSharedDeps.distFilename, }, }); diff --git a/src/legacy/utils/case_conversion.test.ts b/src/legacy/utils/case_conversion.test.ts deleted file mode 100644 index 27498f3878980..0000000000000 --- a/src/legacy/utils/case_conversion.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from './case_conversion'; - -describe('keysToSnakeCaseShallow', () => { - it("should convert all of an object's keys to snake case", () => { - const result = keysToSnakeCaseShallow({ - camelCase: 'camel_case', - 'kebab-case': 'kebab_case', - snake_case: 'snake_case', - }); - - expect(result).toMatchInlineSnapshot(` -Object { - "camel_case": "camel_case", - "kebab_case": "kebab_case", - "snake_case": "snake_case", -} -`); - }); -}); - -describe('keysToCamelCaseShallow', () => { - it("should convert all of an object's keys to camel case", () => { - const result = keysToCamelCaseShallow({ - camelCase: 'camelCase', - 'kebab-case': 'kebabCase', - snake_case: 'snakeCase', - }); - - expect(result).toMatchInlineSnapshot(` -Object { - "camelCase": "camelCase", - "kebabCase": "kebabCase", - "snakeCase": "snakeCase", -} -`); - }); -}); diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 9a21a4b1d5439..efff7f0aa2b46 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -19,6 +19,7 @@ import { writeFile } from 'fs'; import os from 'os'; + import Boom from 'boom'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import TerserPlugin from 'terser-webpack-plugin'; @@ -26,10 +27,10 @@ import webpack from 'webpack'; import Stats from 'webpack/lib/Stats'; import * as threadLoader from 'thread-loader'; import webpackMerge from 'webpack-merge'; -import { DynamicDllPlugin } from './dynamic_dll_plugin'; import WrapperPlugin from 'wrapper-webpack-plugin'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { defaults } from 'lodash'; +import { DynamicDllPlugin } from './dynamic_dll_plugin'; import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; @@ -403,6 +404,10 @@ export default class BaseOptimizer { // and not for the webpack compilations performance itself hints: false, }, + + externals: { + ...UiSharedDeps.externals, + }, }; // when running from the distributable define an environment variable we can use @@ -417,17 +422,6 @@ export default class BaseOptimizer { ], }; - // We need to add react-addons (and a few other bits) for enzyme to work. - // https://github.com/airbnb/enzyme/blob/master/docs/guides/webpack.md - const supportEnzymeConfig = { - externals: { - mocha: 'mocha', - 'react/lib/ExecutionEnvironment': true, - 'react/addons': true, - 'react/lib/ReactContext': true, - }, - }; - const watchingConfig = { plugins: [ new webpack.WatchIgnorePlugin([ @@ -482,9 +476,7 @@ export default class BaseOptimizer { IS_CODE_COVERAGE ? coverageConfig : {}, commonConfig, IS_KIBANA_DISTRIBUTABLE ? isDistributableConfig : {}, - this.uiBundles.isDevMode() - ? webpackMerge(watchingConfig, supportEnzymeConfig) - : productionConfig + this.uiBundles.isDevMode() ? watchingConfig : productionConfig ) ); } @@ -515,22 +507,19 @@ export default class BaseOptimizer { } failedStatsToError(stats) { - const details = stats.toString( - defaults( - { colors: true, warningsFilter: STATS_WARNINGS_FILTER }, - Stats.presetToOptions('minimal') - ) - ); + const details = stats.toString({ + ...Stats.presetToOptions('minimal'), + colors: true, + warningsFilter: STATS_WARNINGS_FILTER, + }); return Boom.internal( `Optimizations failure.\n${details.split('\n').join('\n ')}\n`, - stats.toJson( - defaults({ - warningsFilter: STATS_WARNINGS_FILTER, - ...Stats.presetToOptions('detailed'), - maxModules: 1000, - }) - ) + stats.toJson({ + warningsFilter: STATS_WARNINGS_FILTER, + ...Stats.presetToOptions('detailed'), + maxModules: 1000, + }) ); } diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js index d3c08fae92264..f0261d44e0347 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.js @@ -19,6 +19,7 @@ import { isAbsolute, extname } from 'path'; import LruCache from 'lru-cache'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { createDynamicAssetResponse } from './dynamic_asset_response'; /** @@ -66,6 +67,12 @@ export function createBundlesRoute({ } return [ + buildRouteForBundles( + `${basePublicPath}/bundles/kbn-ui-shared-deps/`, + '/bundles/kbn-ui-shared-deps/', + UiSharedDeps.distDir, + fileHashCache + ), buildRouteForBundles( `${basePublicPath}/bundles/`, '/bundles/', diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js index 2a3d3dd659c67..ecf5def5aa6ca 100644 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js @@ -23,6 +23,7 @@ import webpack from 'webpack'; import webpackMerge from 'webpack-merge'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import TerserPlugin from 'terser-webpack-plugin'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; function generateDLL(config) { const { @@ -145,6 +146,9 @@ function generateDLL(config) { // and not for the webpack compilations performance itself hints: false, }, + externals: { + ...UiSharedDeps.externals, + }, }; } diff --git a/src/optimize/watch/watch_cache.ts b/src/optimize/watch/watch_cache.ts index ab11a8c5d2f11..15957210b3d43 100644 --- a/src/optimize/watch/watch_cache.ts +++ b/src/optimize/watch/watch_cache.ts @@ -18,17 +18,18 @@ */ import { createHash } from 'crypto'; -import { readFile, writeFile } from 'fs'; +import { readFile, writeFile, readdir, unlink, rmdir } from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; - +import path from 'path'; import del from 'del'; -import deleteEmpty from 'delete-empty'; -import globby from 'globby'; import normalizePosixPath from 'normalize-path'; const readAsync = promisify(readFile); const writeAsync = promisify(writeFile); +const readdirAsync = promisify(readdir); +const unlinkAsync = promisify(unlink); +const rmdirAsync = promisify(rmdir); interface Params { logWithMetadata: (tags: string[], message: string, metadata?: { [key: string]: any }) => void; @@ -95,11 +96,7 @@ export class WatchCache { await del(this.statePath, { force: true }); // delete everything in optimize/.cache directory - await del(await globby([normalizePosixPath(this.cachePath)], { dot: true })); - - // delete some empty folder that could be left - // from the previous cache path reset action - await deleteEmpty(this.cachePath); + await recursiveDelete(normalizePosixPath(this.cachePath)); // delete dlls await del(this.dllsPath); @@ -167,3 +164,28 @@ export class WatchCache { } } } + +/** + * Recursively deletes a folder. This is a workaround for a bug in `del` where + * very large folders (with 84K+ files) cause a stack overflow. + */ +async function recursiveDelete(directory: string) { + const entries = await readdirAsync(directory, { withFileTypes: true }); + await Promise.all( + entries.map(entry => { + const absolutePath = path.join(directory, entry.name); + const result = entry.isDirectory() + ? recursiveDelete(absolutePath) + : unlinkAsync(absolutePath); + + // Ignore errors, if the file or directory doesn't exist. + return result.catch(e => { + if (e.code !== 'ENOENT') { + throw e; + } + }); + }) + ); + + return rmdirAsync(directory); +} diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss index b446f1e57a895..bb95840676969 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss @@ -1,9 +1,7 @@ .dshDashboardViewport { - height: 100%; width: 100%; } .dshDashboardViewport-withMargins { width: 100%; - height: 100%; } diff --git a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts index 7c90119fcc1bc..0d5cd6ea17f16 100644 --- a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts @@ -41,7 +41,7 @@ const grammarRuleTranslations: Record = { interface KQLSyntaxErrorData extends Error { found: string; - expected: KQLSyntaxErrorExpected[]; + expected: KQLSyntaxErrorExpected[] | null; location: any; } @@ -53,19 +53,22 @@ export class KQLSyntaxError extends Error { shortMessage: string; constructor(error: KQLSyntaxErrorData, expression: any) { - const translatedExpectations = error.expected.map(expected => { - return grammarRuleTranslations[expected.description] || expected.description; - }); + let message = error.message; + if (error.expected) { + const translatedExpectations = error.expected.map(expected => { + return grammarRuleTranslations[expected.description] || expected.description; + }); - const translatedExpectationText = translatedExpectations.join(', '); + const translatedExpectationText = translatedExpectations.join(', '); - const message = i18n.translate('data.common.esQuery.kql.errors.syntaxError', { - defaultMessage: 'Expected {expectedList} but {foundInput} found.', - values: { - expectedList: translatedExpectationText, - foundInput: error.found ? `"${error.found}"` : endOfInputText, - }, - }); + message = i18n.translate('data.common.esQuery.kql.errors.syntaxError', { + defaultMessage: 'Expected {expectedList} but {foundInput} found.', + values: { + expectedList: translatedExpectationText, + foundInput: error.found ? `"${error.found}"` : endOfInputText, + }, + }); + } const fullMessage = [message, expression, repeat('-', error.location.start.offset) + '^'].join( '\n' diff --git a/src/plugins/data/common/field_formats/converters/bytes.ts b/src/plugins/data/common/field_formats/converters/bytes.ts index 6c6df5eb7367d..f1110add3e7de 100644 --- a/src/plugins/data/common/field_formats/converters/bytes.ts +++ b/src/plugins/data/common/field_formats/converters/bytes.ts @@ -26,4 +26,5 @@ export class BytesFormat extends NumeralFormat { id = BytesFormat.id; title = BytesFormat.title; + allowsNumericalAggregations = true; } diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index d02de1a2fd889..8caa11be5ca79 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -171,6 +171,7 @@ export class DurationFormat extends FieldFormat { static fieldType = KBN_FIELD_TYPES.NUMBER; static inputFormats = inputFormats; static outputFormats = outputFormats; + allowsNumericalAggregations = true; isHuman() { return this.param('outputFormat') === HUMAN_FRIENDLY; diff --git a/src/plugins/data/common/field_formats/converters/number.ts b/src/plugins/data/common/field_formats/converters/number.ts index 6969c1551e1cc..686329e887682 100644 --- a/src/plugins/data/common/field_formats/converters/number.ts +++ b/src/plugins/data/common/field_formats/converters/number.ts @@ -26,4 +26,5 @@ export class NumberFormat extends NumeralFormat { id = NumberFormat.id; title = NumberFormat.title; + allowsNumericalAggregations = true; } diff --git a/src/plugins/data/common/field_formats/converters/percent.ts b/src/plugins/data/common/field_formats/converters/percent.ts index 2ae32c7c77f07..d839a54dd0c2c 100644 --- a/src/plugins/data/common/field_formats/converters/percent.ts +++ b/src/plugins/data/common/field_formats/converters/percent.ts @@ -26,6 +26,7 @@ export class PercentFormat extends NumeralFormat { id = PercentFormat.id; title = PercentFormat.title; + allowsNumericalAggregations = true; getParamDefaults = () => ({ pattern: this.getConfig!('format:percent:defaultPattern'), diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/data/common/utils/index.ts index 7196c96989e97..c5f1276feb81d 100644 --- a/src/plugins/data/common/utils/index.ts +++ b/src/plugins/data/common/utils/index.ts @@ -18,3 +18,4 @@ */ export { shortenDottedString } from './shorten_dotted_string'; +export { parseInterval } from './parse_interval'; diff --git a/src/plugins/data/common/utils/parse_interval.test.ts b/src/plugins/data/common/utils/parse_interval.test.ts new file mode 100644 index 0000000000000..0c02b02a25af0 --- /dev/null +++ b/src/plugins/data/common/utils/parse_interval.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { Duration, unitOfTime } from 'moment'; +import { parseInterval } from './parse_interval'; + +const validateDuration = (duration: Duration | null, unit: unitOfTime.Base, value: number) => { + expect(duration).toBeDefined(); + + if (duration) { + expect(duration.as(unit)).toBe(value); + } +}; + +describe('parseInterval', () => { + describe('integer', () => { + test('should correctly parse 1d interval', () => { + validateDuration(parseInterval('1d'), 'd', 1); + }); + + test('should correctly parse 2y interval', () => { + validateDuration(parseInterval('2y'), 'y', 2); + }); + + test('should correctly parse 5M interval', () => { + validateDuration(parseInterval('5M'), 'M', 5); + }); + + test('should correctly parse 5m interval', () => { + validateDuration(parseInterval('5m'), 'm', 5); + }); + + test('should correctly parse 250ms interval', () => { + validateDuration(parseInterval('250ms'), 'ms', 250); + }); + + test('should correctly parse 100s interval', () => { + validateDuration(parseInterval('100s'), 's', 100); + }); + + test('should correctly parse 23d interval', () => { + validateDuration(parseInterval('23d'), 'd', 23); + }); + + test('should correctly parse 52w interval', () => { + validateDuration(parseInterval('52w'), 'w', 52); + }); + }); + + describe('fractional interval', () => { + test('should correctly parse fractional 2.35y interval', () => { + validateDuration(parseInterval('2.35y'), 'y', 2.35); + }); + + test('should correctly parse fractional 1.5w interval', () => { + validateDuration(parseInterval('1.5w'), 'w', 1.5); + }); + }); + + describe('less than 1', () => { + test('should correctly bubble up 0.5h interval which are less than 1', () => { + validateDuration(parseInterval('0.5h'), 'm', 30); + }); + + test('should correctly bubble up 0.5d interval which are less than 1', () => { + validateDuration(parseInterval('0.5d'), 'h', 12); + }); + }); + + describe('unit in an interval only', () => { + test('should correctly parse ms interval', () => { + validateDuration(parseInterval('ms'), 'ms', 1); + }); + + test('should correctly parse d interval', () => { + validateDuration(parseInterval('d'), 'd', 1); + }); + + test('should correctly parse m interval', () => { + validateDuration(parseInterval('m'), 'm', 1); + }); + + test('should correctly parse y interval', () => { + validateDuration(parseInterval('y'), 'y', 1); + }); + + test('should correctly parse M interval', () => { + validateDuration(parseInterval('M'), 'M', 1); + }); + }); + + test('should return null for an invalid interval', () => { + let duration = parseInterval(''); + expect(duration).toBeNull(); + + // @ts-ignore + duration = parseInterval(null); + expect(duration).toBeNull(); + + duration = parseInterval('234asdf'); + expect(duration).toBeNull(); + }); +}); diff --git a/src/legacy/ui/public/utils/parse_interval.js b/src/plugins/data/common/utils/parse_interval.ts similarity index 86% rename from src/legacy/ui/public/utils/parse_interval.js rename to src/plugins/data/common/utils/parse_interval.ts index fe484985ef101..ef1d89e400b72 100644 --- a/src/legacy/ui/public/utils/parse_interval.js +++ b/src/plugins/data/common/utils/parse_interval.ts @@ -17,14 +17,14 @@ * under the License. */ -import _ from 'lodash'; -import moment from 'moment'; +import { find } from 'lodash'; +import moment, { unitOfTime } from 'moment'; import dateMath from '@elastic/datemath'; // Assume interval is in the form (value)(unit), such as "1h" const INTERVAL_STRING_RE = new RegExp('^([0-9\\.]*)\\s*(' + dateMath.units.join('|') + ')$'); -export function parseInterval(interval) { +export function parseInterval(interval: string): moment.Duration | null { const matches = String(interval) .trim() .match(INTERVAL_STRING_RE); @@ -33,7 +33,7 @@ export function parseInterval(interval) { try { const value = parseFloat(matches[1]) || 1; - const unit = matches[2]; + const unit = matches[2] as unitOfTime.Base; const duration = moment.duration(value, unit); @@ -44,9 +44,10 @@ export function parseInterval(interval) { // adding 0.5 days until we hit the end date. However, since there is a bug in moment, when you add 0.5 days to // the start date, you get the same exact date (instead of being ahead by 12 hours). So instead of returning // a duration corresponding to 0.5 hours, we return a duration corresponding to 12 hours. - const selectedUnit = _.find(dateMath.units, function(unit) { - return Math.abs(duration.as(unit)) >= 1; - }); + const selectedUnit = find( + dateMath.units, + u => Math.abs(duration.as(u)) >= 1 + ) as unitOfTime.Base; return moment.duration(duration.as(selectedUnit), selectedUnit); } catch (e) { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 967887764237d..4b330600417e7 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -90,6 +90,8 @@ export { castEsToKbnFieldTypeName, getKbnFieldType, getKbnTypeNames, + // utils + parseInterval, } from '../common'; // Export plugin after all other imports diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 2fce33793cd46..6f5f9b3956187 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -346,6 +346,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "remove": [MockFunction], "replace": [MockFunction], }, + "openConfirm": [MockFunction], "openFlyout": [MockFunction], "openModal": [MockFunction], }, @@ -965,6 +966,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "remove": [MockFunction], "replace": [MockFunction], }, + "openConfirm": [MockFunction], "openFlyout": [MockFunction], "openModal": [MockFunction], }, @@ -1572,6 +1574,7 @@ exports[`QueryStringInput Should pass the query language to the language switche "remove": [MockFunction], "replace": [MockFunction], }, + "openConfirm": [MockFunction], "openFlyout": [MockFunction], "openModal": [MockFunction], }, @@ -2188,6 +2191,7 @@ exports[`QueryStringInput Should pass the query language to the language switche "remove": [MockFunction], "replace": [MockFunction], }, + "openConfirm": [MockFunction], "openFlyout": [MockFunction], "openModal": [MockFunction], }, @@ -2795,6 +2799,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` "remove": [MockFunction], "replace": [MockFunction], }, + "openConfirm": [MockFunction], "openFlyout": [MockFunction], "openModal": [MockFunction], }, @@ -3411,6 +3416,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` "remove": [MockFunction], "replace": [MockFunction], }, + "openConfirm": [MockFunction], "openFlyout": [MockFunction], "openModal": [MockFunction], }, diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index fe96c494bd9ff..3cd088744a439 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -18,7 +18,7 @@ */ import { PluginInitializerContext } from '../../../core/server'; -import { DataServerPlugin } from './plugin'; +import { DataServerPlugin, DataPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new DataServerPlugin(initializerContext); @@ -47,6 +47,8 @@ export { // timefilter RefreshInterval, TimeRange, + // utils + parseInterval, } from '../common'; /** @@ -91,4 +93,4 @@ export { getKbnTypeNames, } from '../common'; -export { DataServerPlugin as Plugin }; +export { DataServerPlugin as Plugin, DataPluginSetup as PluginSetup }; diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index be142f2cc74e6..a179be6946c76 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -91,7 +91,7 @@ function DevToolsWrapper({ if (mountedTool.current) { mountedTool.current.unmountHandler(); } - const params = { element, appBasePath: '' }; + const params = { element, appBasePath: '', onAppLeave: () => undefined }; const unmountHandler = isAppMountDeprecated(activeDevTool.mount) ? await activeDevTool.mount(appMountContext, params) : await activeDevTool.mount(params); diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 89dea53d75b38..5b9a6dc9de002 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -37,6 +37,7 @@ import { RadioGroupField, RangeField, SelectField, + SuperSelectField, ToggleField, } from './fields'; @@ -50,6 +51,7 @@ const mapTypeToFieldComponent = { [FIELD_TYPES.RADIO_GROUP]: RadioGroupField, [FIELD_TYPES.RANGE]: RangeField, [FIELD_TYPES.SELECT]: SelectField, + [FIELD_TYPES.SUPER_SELECT]: SuperSelectField, [FIELD_TYPES.TOGGLE]: ToggleField, }; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/index.ts b/src/plugins/es_ui_shared/static/forms/components/fields/index.ts index f973bb7b04d34..35635d0e8530c 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/index.ts +++ b/src/plugins/es_ui_shared/static/forms/components/fields/index.ts @@ -25,5 +25,6 @@ export * from './multi_select_field'; export * from './radio_group_field'; export * from './range_field'; export * from './select_field'; +export * from './super_select_field'; export * from './toggle_field'; export * from './text_area_field'; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx new file mode 100644 index 0000000000000..9b29d75230d7a --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFormRow, EuiSuperSelect } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; + +interface Props { + field: FieldHook; + euiFieldProps?: Record; + idAria?: string; + [key: string]: any; +} + +export const SuperSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + { + field.setValue(value); + }} + options={[]} + isInvalid={isInvalid} + data-test-subj="select" + {...euiFieldProps} + /> + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts index df2807e59ab46..4056947483107 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts @@ -28,6 +28,7 @@ export const FIELD_TYPES = { RADIO_GROUP: 'radioGroup', RANGE: 'range', SELECT: 'select', + SUPER_SELECT: 'superSelect', MULTI_SELECT: 'multiSelect', }; diff --git a/src/plugins/inspector/public/views/data/components/download_options.tsx b/src/plugins/inspector/public/views/data/components/download_options.tsx index 6d21dcdafa84d..e7bfbed23c074 100644 --- a/src/plugins/inspector/public/views/data/components/download_options.tsx +++ b/src/plugins/inspector/public/views/data/components/download_options.tsx @@ -20,6 +20,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { DataViewColumn, DataViewRow } from '../types'; @@ -66,8 +67,14 @@ class DataDownloadOptions extends Component { + let filename = this.props.title; + if (!filename || filename.length === 0) { + filename = i18n.translate('inspector.data.downloadOptionsUnsavedFilename', { + defaultMessage: 'unsaved', + }); + } exportAsCsv({ - filename: `${this.props.title}.csv`, + filename: `${filename}.csv`, columns: this.props.columns, rows: this.props.rows, csvSeparator: this.props.csvSeparator, diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 0ae77995c0502..62440f12c6d84 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -78,6 +78,13 @@ export interface Props { */ hoverProvider?: monacoEditor.languages.HoverProvider; + /** + * Language config provider for bracket + * Documentation for the provider can be found here: + * https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html + */ + languageConfiguration?: monacoEditor.languages.LanguageConfiguration; + /** * Function called before the editor is mounted in the view */ @@ -130,6 +137,13 @@ export class CodeEditor extends React.Component { if (this.props.hoverProvider) { monaco.languages.registerHoverProvider(this.props.languageId, this.props.hoverProvider); } + + if (this.props.languageConfiguration) { + monaco.languages.setLanguageConfiguration( + this.props.languageId, + this.props.languageConfiguration + ); + } }); // Register the theme diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 10b7dd2b4da44..cfe89f16e99dd 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -25,4 +25,5 @@ export * from './overlays'; export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; +export { useUrlTracker } from './use_url_tracker'; export { toMountPoint } from './util'; diff --git a/src/legacy/ui/public/management/components/index.ts b/src/plugins/kibana_react/public/use_url_tracker/index.ts similarity index 94% rename from src/legacy/ui/public/management/components/index.ts rename to src/plugins/kibana_react/public/use_url_tracker/index.ts index e3a18ec4e2698..fdceaf34e04ee 100644 --- a/src/legacy/ui/public/management/components/index.ts +++ b/src/plugins/kibana_react/public/use_url_tracker/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SidebarNav } from './sidebar_nav'; +export { useUrlTracker } from './use_url_tracker'; diff --git a/src/plugins/kibana_react/public/use_url_tracker/use_url_tracker.test.tsx b/src/plugins/kibana_react/public/use_url_tracker/use_url_tracker.test.tsx new file mode 100644 index 0000000000000..d1425a09b2f9c --- /dev/null +++ b/src/plugins/kibana_react/public/use_url_tracker/use_url_tracker.test.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useUrlTracker } from './use_url_tracker'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { createMemoryHistory } from 'history'; + +describe('useUrlTracker', () => { + const key = 'key'; + let storage = new StubBrowserStorage(); + let history = createMemoryHistory(); + beforeEach(() => { + storage = new StubBrowserStorage(); + history = createMemoryHistory(); + }); + + it('should track history changes and save them to storage', () => { + expect(storage.getItem(key)).toBeNull(); + const { unmount } = renderHook(() => { + useUrlTracker(key, history, () => false, storage); + }); + expect(storage.getItem(key)).toBe('/'); + history.push('/change'); + expect(storage.getItem(key)).toBe('/change'); + unmount(); + history.push('/other-change'); + expect(storage.getItem(key)).toBe('/change'); + }); + + it('by default should restore initial url', () => { + storage.setItem(key, '/change'); + renderHook(() => { + useUrlTracker(key, history, undefined, storage); + }); + expect(history.location.pathname).toBe('/change'); + }); + + it('should restore initial url if shouldRestoreUrl cb returns true', () => { + storage.setItem(key, '/change'); + renderHook(() => { + useUrlTracker(key, history, () => true, storage); + }); + expect(history.location.pathname).toBe('/change'); + }); + + it('should not restore initial url if shouldRestoreUrl cb returns false', () => { + storage.setItem(key, '/change'); + renderHook(() => { + useUrlTracker(key, history, () => false, storage); + }); + expect(history.location.pathname).toBe('/'); + }); +}); diff --git a/src/plugins/kibana_react/public/use_url_tracker/use_url_tracker.tsx b/src/plugins/kibana_react/public/use_url_tracker/use_url_tracker.tsx new file mode 100644 index 0000000000000..97e69fe22a842 --- /dev/null +++ b/src/plugins/kibana_react/public/use_url_tracker/use_url_tracker.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { History } from 'history'; +import { useLayoutEffect } from 'react'; +import { createUrlTracker } from '../../../kibana_utils/public/'; + +/** + * State management url_tracker in react hook form + * + * Replicates what src/legacy/ui/public/chrome/api/nav.ts did + * Persists the url in sessionStorage so it could be restored if navigated back to the app + * + * @param key - key to use in storage + * @param history - history instance to use + * @param shouldRestoreUrl - cb if url should be restored + * @param storage - storage to use. window.sessionStorage is default + */ +export function useUrlTracker( + key: string, + history: History, + shouldRestoreUrl: (urlToRestore: string) => boolean = () => true, + storage: Storage = sessionStorage +) { + useLayoutEffect(() => { + const urlTracker = createUrlTracker(key, storage); + const urlToRestore = urlTracker.getTrackedUrl(); + if (urlToRestore && shouldRestoreUrl(urlToRestore)) { + history.replace(urlToRestore); + } + const stopTrackingUrl = urlTracker.startTrackingUrl(history); + return () => { + stopTrackingUrl(); + }; + }, [key, history]); +} diff --git a/src/plugins/kibana_utils/common/distinct_until_changed_with_initial_value.test.ts b/src/plugins/kibana_utils/common/distinct_until_changed_with_initial_value.test.ts new file mode 100644 index 0000000000000..24f8f13f21478 --- /dev/null +++ b/src/plugins/kibana_utils/common/distinct_until_changed_with_initial_value.test.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Subject } from 'rxjs'; +import { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; +import { toArray } from 'rxjs/operators'; +import deepEqual from 'fast-deep-equal'; + +describe('distinctUntilChangedWithInitialValue', () => { + it('should skip updates with the same value', async () => { + const subject = new Subject(); + const result = subject.pipe(distinctUntilChangedWithInitialValue(1), toArray()).toPromise(); + + subject.next(2); + subject.next(3); + subject.next(3); + subject.next(3); + subject.complete(); + + expect(await result).toEqual([2, 3]); + }); + + it('should accept promise as initial value', async () => { + const subject = new Subject(); + const result = subject + .pipe( + distinctUntilChangedWithInitialValue( + new Promise(resolve => { + resolve(1); + setTimeout(() => { + subject.next(2); + subject.next(3); + subject.next(3); + subject.next(3); + subject.complete(); + }); + }) + ), + toArray() + ) + .toPromise(); + expect(await result).toEqual([2, 3]); + }); + + it('should accept custom comparator', async () => { + const subject = new Subject(); + const result = subject + .pipe(distinctUntilChangedWithInitialValue({ test: 1 }, deepEqual), toArray()) + .toPromise(); + + subject.next({ test: 1 }); + subject.next({ test: 2 }); + subject.next({ test: 2 }); + subject.next({ test: 3 }); + subject.complete(); + + expect(await result).toEqual([{ test: 2 }, { test: 3 }]); + }); +}); diff --git a/src/plugins/kibana_utils/common/distinct_until_changed_with_initial_value.ts b/src/plugins/kibana_utils/common/distinct_until_changed_with_initial_value.ts new file mode 100644 index 0000000000000..6af9cc1e8ac3a --- /dev/null +++ b/src/plugins/kibana_utils/common/distinct_until_changed_with_initial_value.ts @@ -0,0 +1,42 @@ +/* + * 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 { MonoTypeOperatorFunction, queueScheduler, scheduled, from } from 'rxjs'; +import { concatAll, distinctUntilChanged, skip } from 'rxjs/operators'; + +export function distinctUntilChangedWithInitialValue( + initialValue: T | Promise, + compare?: (x: T, y: T) => boolean +): MonoTypeOperatorFunction { + return input$ => + scheduled( + [isPromise(initialValue) ? from(initialValue) : [initialValue], input$], + queueScheduler + ).pipe(concatAll(), distinctUntilChanged(compare), skip(1)); +} + +function isPromise(value: T | Promise): value is Promise { + return ( + !!value && + typeof value === 'object' && + 'then' in value && + typeof value.then === 'function' && + !('subscribe' in value) + ); +} diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index d13a250cedf2e..eb3bb96c8e874 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -18,3 +18,4 @@ */ export * from './defer'; +export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/demos/demos.test.ts b/src/plugins/kibana_utils/demos/demos.test.ts index 4e792ceef117a..5c50e152ad46c 100644 --- a/src/plugins/kibana_utils/demos/demos.test.ts +++ b/src/plugins/kibana_utils/demos/demos.test.ts @@ -19,6 +19,7 @@ import { result as counterResult } from './state_containers/counter'; import { result as todomvcResult } from './state_containers/todomvc'; +import { result as urlSyncResult } from './state_sync/url'; describe('demos', () => { describe('state containers', () => { @@ -33,4 +34,12 @@ describe('demos', () => { ]); }); }); + + describe('state sync', () => { + test('url sync demo works', async () => { + expect(await urlSyncResult).toMatchInlineSnapshot( + `"http://localhost/#?_s=!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test))"` + ); + }); + }); }); diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts new file mode 100644 index 0000000000000..657b64f55a776 --- /dev/null +++ b/src/plugins/kibana_utils/demos/state_sync/url.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc'; +import { BaseStateContainer, createStateContainer } from '../../public/state_containers'; +import { + createKbnUrlStateStorage, + syncState, + INullableBaseStateContainer, +} from '../../public/state_sync'; + +const tick = () => new Promise(resolve => setTimeout(resolve)); + +const stateContainer = createStateContainer(defaultState, pureTransitions); +const { start, stop } = syncState({ + stateContainer: withDefaultState(stateContainer, defaultState), + storageKey: '_s', + stateStorage: createKbnUrlStateStorage(), +}); + +start(); +export const result = Promise.resolve() + .then(() => { + // http://localhost/#?_s=!((completed:!f,id:0,text:'Learning+state+containers')" + + stateContainer.transitions.add({ + id: 2, + text: 'test', + completed: false, + }); + + // http://localhost/#?_s=!((completed:!f,id:0,text:'Learning+state+containers'),(completed:!f,id:2,text:test))" + + /* actual url updates happens async */ + return tick(); + }) + .then(() => { + stop(); + return window.location.href; + }); + +function withDefaultState( + // eslint-disable-next-line no-shadow + stateContainer: BaseStateContainer, + // eslint-disable-next-line no-shadow + defaultState: State +): INullableBaseStateContainer { + return { + ...stateContainer, + set: (state: State | null) => { + stateContainer.set(state || defaultState); + }, + }; +} diff --git a/src/plugins/kibana_utils/public/field_mapping/mapping_setup.ts b/src/plugins/kibana_utils/public/field_mapping/mapping_setup.ts index 72f3716147efa..99b49b401a8b8 100644 --- a/src/plugins/kibana_utils/public/field_mapping/mapping_setup.ts +++ b/src/plugins/kibana_utils/public/field_mapping/mapping_setup.ts @@ -19,7 +19,9 @@ import { mapValues, isString } from 'lodash'; import { FieldMappingSpec, MappingObject } from './types'; -import { ES_FIELD_TYPES } from '../../../data/public'; + +// import from ./common/types to prevent circular dependency of kibana_utils <-> data plugin +import { ES_FIELD_TYPES } from '../../../data/common/types'; /** @private */ type ShorthandFieldMapObject = FieldMappingSpec | ES_FIELD_TYPES | 'json'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index af2fc9e31b21b..0ba444c4e9395 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -27,6 +27,34 @@ export * from './render_complete'; export * from './resize_checker'; export * from './state_containers'; export * from './storage'; -export * from './storage/hashed_item_store'; -export * from './state_management/state_hash'; -export * from './state_management/url'; +export { hashedItemStore, HashedItemStore } from './storage/hashed_item_store'; +export { + createStateHash, + persistState, + retrieveState, + isStateHash, +} from './state_management/state_hash'; +export { + hashQuery, + hashUrl, + unhashUrl, + unhashQuery, + createUrlTracker, + createKbnUrlControls, + getStateFromKbnUrl, + getStatesFromKbnUrl, + setStateToKbnUrl, +} from './state_management/url'; +export { + syncState, + syncStates, + createKbnUrlStateStorage, + createSessionStorageStateStorage, + IStateSyncConfig, + ISyncStateRef, + IKbnUrlStateStorage, + INullableBaseStateContainer, + ISessionStorageStateStorage, + StartSyncStateFnType, + StopSyncStateFnType, +} from './state_sync'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts index 9165181299a90..95f4c35f2ce01 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts @@ -113,6 +113,13 @@ test('multiple subscribers can subscribe', () => { expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); }); +test('can create state container without transitions', () => { + const state = { foo: 'bar' }; + const stateContainer = createStateContainer(state); + expect(stateContainer.transitions).toEqual({}); + expect(stateContainer.get()).toEqual(state); +}); + test('creates impure mutators from pure mutators', () => { const { mutators } = create( {}, diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts index 1ef4a1c012817..b949a9daed0ae 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts @@ -41,11 +41,11 @@ const freeze: (value: T) => RecursiveReadonly = export const createStateContainer = < State, - PureTransitions extends object, + PureTransitions extends object = {}, PureSelectors extends object = {} >( defaultState: State, - pureTransitions: PureTransitions, + pureTransitions: PureTransitions = {} as PureTransitions, pureSelectors: PureSelectors = {} as PureSelectors ): ReduxLikeStateContainer => { const data$ = new BehaviorSubject>(freeze(defaultState)); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx index 8f5810f3e147d..c1a35441b637b 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx @@ -193,12 +193,7 @@ describe('hooks', () => { describe('useTransitions', () => { test('useTransitions hook returns mutations that can update state', () => { - const { store } = create< - { - cnt: number; - }, - any - >( + const { store } = create( { cnt: 0, }, diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts index e94165cc48376..45b34b13251f4 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts @@ -35,7 +35,7 @@ export const createStateContainerReactHelpers = useContainer().transitions; + const useTransitions = (): Container['transitions'] => useContainer().transitions; const useSelector = ( selector: (state: UnboxState) => Result, diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/public/state_containers/types.ts index e0a1a18972635..e120f60e72b8f 100644 --- a/src/plugins/kibana_utils/public/state_containers/types.ts +++ b/src/plugins/kibana_utils/public/state_containers/types.ts @@ -42,7 +42,7 @@ export interface BaseStateContainer { export interface StateContainer< State, - PureTransitions extends object, + PureTransitions extends object = {}, PureSelectors extends object = {} > extends BaseStateContainer { transitions: Readonly>; @@ -51,7 +51,7 @@ export interface StateContainer< export interface ReduxLikeStateContainer< State, - PureTransitions extends object, + PureTransitions extends object = {}, PureSelectors extends object = {} > extends StateContainer { getState: () => RecursiveReadonly; diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts new file mode 100644 index 0000000000000..c535e965aa772 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts @@ -0,0 +1,61 @@ +/* + * 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 rison, { RisonValue } from 'rison-node'; +import { isStateHash, retrieveState, persistState } from '../state_hash'; + +// should be: +// export function decodeState(expandedOrHashedState: string) +// but this leads to the chain of types mismatches up to BaseStateContainer interfaces, +// as in state containers we don't have any restrictions on state shape +export function decodeState(expandedOrHashedState: string): State { + if (isStateHash(expandedOrHashedState)) { + return retrieveState(expandedOrHashedState); + } else { + return (rison.decode(expandedOrHashedState) as unknown) as State; + } +} + +// should be: +// export function encodeState(expandedOrHashedState: string) +// but this leads to the chain of types mismatches up to BaseStateContainer interfaces, +// as in state containers we don't have any restrictions on state shape +export function encodeState(state: State, useHash: boolean): string { + if (useHash) { + return persistState(state); + } else { + return rison.encode((state as unknown) as RisonValue); + } +} + +export function hashedStateToExpandedState(expandedOrHashedState: string): string { + if (isStateHash(expandedOrHashedState)) { + return encodeState(retrieveState(expandedOrHashedState), false); + } + + return expandedOrHashedState; +} + +export function expandedStateToHashedState(expandedOrHashedState: string): string { + if (isStateHash(expandedOrHashedState)) { + return expandedOrHashedState; + } + + return persistState(decodeState(expandedOrHashedState)); +} diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts new file mode 100644 index 0000000000000..da1382720faff --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/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 { + encodeState, + decodeState, + expandedStateToHashedState, + hashedStateToExpandedState, +} from './encode_decode_state'; diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/index.ts b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts index 0e52c4c55872d..24c3c57613477 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/index.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './state_hash'; +export { isStateHash, createStateHash, persistState, retrieveState } from './state_hash'; diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts index a3eb5272b112d..f56d71297c030 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { Sha256 } from '../../../../../core/public/utils'; import { hashedItemStore } from '../../storage/hashed_item_store'; @@ -52,3 +53,46 @@ export function createStateHash( export function isStateHash(str: string) { return String(str).indexOf(HASH_PREFIX) === 0; } + +export function retrieveState(stateHash: string): State { + const json = hashedItemStore.getItem(stateHash); + const throwUnableToRestoreUrlError = () => { + throw new Error( + i18n.translate('kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage', { + defaultMessage: + 'Unable to completely restore the URL, be sure to use the share functionality.', + }) + ); + }; + if (json === null) { + return throwUnableToRestoreUrlError(); + } + try { + return JSON.parse(json); + } catch (e) { + return throwUnableToRestoreUrlError(); + } +} + +export function persistState(state: State): string { + const json = JSON.stringify(state); + const hash = createStateHash(json); + + const isItemSet = hashedItemStore.setItem(hash, json); + if (isItemSet) return hash; + // If we ran out of space trying to persist the state, notify the user. + const message = i18n.translate( + 'kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage', + { + defaultMessage: + 'Kibana is unable to store history items in your session ' + + `because it is full and there don't seem to be items any items safe ` + + 'to delete.\n\n' + + 'This can usually be fixed by moving to a fresh tab, but could ' + + 'be caused by a larger issue. If you are seeing this message regularly, ' + + 'please file an issue at {gitHubIssuesUrl}.', + values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' }, + } + ); + throw new Error(message); +} diff --git a/src/plugins/kibana_utils/public/state_management/url/format.test.ts b/src/plugins/kibana_utils/public/state_management/url/format.test.ts new file mode 100644 index 0000000000000..728f069840c72 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/format.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { replaceUrlHashQuery } from './format'; + +describe('format', () => { + describe('replaceUrlHashQuery', () => { + it('should add hash query to url without hash', () => { + const url = 'http://localhost:5601/oxf/app/kibana'; + expect(replaceUrlHashQuery(url, () => ({ test: 'test' }))).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#?test=test"` + ); + }); + + it('should replace hash query', () => { + const url = 'http://localhost:5601/oxf/app/kibana#?test=test'; + expect( + replaceUrlHashQuery(url, query => ({ + ...query, + test1: 'test1', + })) + ).toMatchInlineSnapshot(`"http://localhost:5601/oxf/app/kibana#?test=test&test1=test1"`); + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/public/state_management/url/format.ts new file mode 100644 index 0000000000000..988ee08627382 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/format.ts @@ -0,0 +1,41 @@ +/* + * 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 { format as formatUrl } from 'url'; +import { ParsedUrlQuery } from 'querystring'; +import { parseUrl, parseUrlHash } from './parse'; +import { stringifyQueryString } from './stringify_query_string'; + +export function replaceUrlHashQuery( + rawUrl: string, + queryReplacer: (query: ParsedUrlQuery) => ParsedUrlQuery +) { + const url = parseUrl(rawUrl); + const hash = parseUrlHash(rawUrl); + const newQuery = queryReplacer(hash?.query || {}); + const searchQueryString = stringifyQueryString(newQuery); + if ((!hash || !hash.search) && !searchQueryString) return rawUrl; // nothing to change. return original url + return formatUrl({ + ...url, + hash: formatUrl({ + pathname: hash?.pathname || '', + search: searchQueryString, + }), + }); +} diff --git a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts index a85158acddefd..ec87b8464ac2d 100644 --- a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts @@ -29,13 +29,6 @@ describe('hash unhash url', () => { describe('hash url', () => { describe('does nothing', () => { - it('if missing input', () => { - expect(() => { - // @ts-ignore - hashUrl(); - }).not.toThrowError(); - }); - it('if url is empty', () => { const url = ''; expect(hashUrl(url)).toBe(url); diff --git a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts index 872e7953f938b..a29f8bb9ac635 100644 --- a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts @@ -17,13 +17,8 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import rison, { RisonObject } from 'rison-node'; -import { stringify as stringifyQueryString } from 'querystring'; -import encodeUriQuery from 'encode-uri-query'; -import { format as formatUrl, parse as parseUrl } from 'url'; -import { hashedItemStore } from '../../storage/hashed_item_store'; -import { createStateHash, isStateHash } from '../state_hash'; +import { expandedStateToHashedState, hashedStateToExpandedState } from '../state_encoder'; +import { replaceUrlHashQuery } from './format'; export type IParsedUrlQuery = Record; @@ -32,8 +27,8 @@ interface IUrlQueryMapperOptions { } export type IUrlQueryReplacerOptions = IUrlQueryMapperOptions; -export const unhashQuery = createQueryMapper(stateHashToRisonState); -export const hashQuery = createQueryMapper(risonStateToStateHash); +export const unhashQuery = createQueryMapper(hashedStateToExpandedState); +export const hashQuery = createQueryMapper(expandedStateToHashedState); export const unhashUrl = createQueryReplacer(unhashQuery); export const hashUrl = createQueryReplacer(hashQuery); @@ -61,97 +56,5 @@ function createQueryReplacer( queryMapper: (q: IParsedUrlQuery, options?: IUrlQueryMapperOptions) => IParsedUrlQuery, options?: IUrlQueryReplacerOptions ) { - return (url: string) => { - if (!url) return url; - - const parsedUrl = parseUrl(url, true); - if (!parsedUrl.hash) return url; - - const appUrl = parsedUrl.hash.slice(1); // trim the # - if (!appUrl) return url; - - const appUrlParsed = parseUrl(appUrl, true); - if (!appUrlParsed.query) return url; - - const changedAppQuery = queryMapper(appUrlParsed.query, options); - - // encodeUriQuery implements the less-aggressive encoding done naturally by - // the browser. We use it to generate the same urls the browser would - const changedAppQueryString = stringifyQueryString(changedAppQuery, undefined, undefined, { - encodeURIComponent: encodeUriQuery, - }); - - return formatUrl({ - ...parsedUrl, - hash: formatUrl({ - pathname: appUrlParsed.pathname, - search: changedAppQueryString, - }), - }); - }; -} - -// TODO: this helper should be merged with or replaced by -// src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts -// maybe to become simplified stateless version -export function retrieveState(stateHash: string): RisonObject { - const json = hashedItemStore.getItem(stateHash); - const throwUnableToRestoreUrlError = () => { - throw new Error( - i18n.translate('kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage', { - defaultMessage: - 'Unable to completely restore the URL, be sure to use the share functionality.', - }) - ); - }; - if (json === null) { - return throwUnableToRestoreUrlError(); - } - try { - return JSON.parse(json); - } catch (e) { - return throwUnableToRestoreUrlError(); - } -} - -// TODO: this helper should be merged with or replaced by -// src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts -// maybe to become simplified stateless version -export function persistState(state: RisonObject): string { - const json = JSON.stringify(state); - const hash = createStateHash(json); - - const isItemSet = hashedItemStore.setItem(hash, json); - if (isItemSet) return hash; - // If we ran out of space trying to persist the state, notify the user. - const message = i18n.translate( - 'kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage', - { - defaultMessage: - 'Kibana is unable to store history items in your session ' + - `because it is full and there don't seem to be items any items safe ` + - 'to delete.\n\n' + - 'This can usually be fixed by moving to a fresh tab, but could ' + - 'be caused by a larger issue. If you are seeing this message regularly, ' + - 'please file an issue at {gitHubIssuesUrl}.', - values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' }, - } - ); - throw new Error(message); -} - -function stateHashToRisonState(stateHashOrRison: string): string { - if (isStateHash(stateHashOrRison)) { - return rison.encode(retrieveState(stateHashOrRison)); - } - - return stateHashOrRison; -} - -function risonStateToStateHash(stateHashOrRison: string): string | null { - if (isStateHash(stateHashOrRison)) { - return stateHashOrRison; - } - - return persistState(rison.decode(stateHashOrRison) as RisonObject); + return (url: string) => replaceUrlHashQuery(url, query => queryMapper(query, options)); } diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts index 30c5696233db7..40491bf7a274b 100644 --- a/src/plugins/kibana_utils/public/state_management/url/index.ts +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -17,4 +17,12 @@ * under the License. */ -export * from './hash_unhash_url'; +export { hashUrl, hashQuery, unhashUrl, unhashQuery } from './hash_unhash_url'; +export { + createKbnUrlControls, + setStateToKbnUrl, + getStateFromKbnUrl, + getStatesFromKbnUrl, + IKbnUrlControls, +} from './kbn_url_storage'; +export { createUrlTracker } from './url_tracker'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts new file mode 100644 index 0000000000000..f1c527d3d5309 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -0,0 +1,246 @@ +/* + * 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 '../../storage/hashed_item_store/mock'; +import { + History, + createBrowserHistory, + createHashHistory, + createMemoryHistory, + createPath, +} from 'history'; +import { + getRelativeToHistoryPath, + createKbnUrlControls, + IKbnUrlControls, + setStateToKbnUrl, + getStateFromKbnUrl, +} from './kbn_url_storage'; + +describe('kbn_url_storage', () => { + describe('getStateFromUrl & setStateToUrl', () => { + const url = 'http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id'; + const state1 = { + testStr: '123', + testNumber: 0, + testObj: { test: '123' }, + testNull: null, + testArray: [1, 2, {}], + }; + const state2 = { + test: '123', + }; + + it('should set expanded state to url', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: false }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"` + ); + const retrievedState1 = getStateFromKbnUrl('_s', newUrl); + expect(retrievedState1).toEqual(state1); + + newUrl = setStateToKbnUrl('_s', state2, { useHash: false }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_s=(test:'123')"` + ); + const retrievedState2 = getStateFromKbnUrl('_s', newUrl); + expect(retrievedState2).toEqual(state2); + }); + + it('should set hashed state to url', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: true }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_s=h@a897fac"` + ); + const retrievedState1 = getStateFromKbnUrl('_s', newUrl); + expect(retrievedState1).toEqual(state1); + + newUrl = setStateToKbnUrl('_s', state2, { useHash: true }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_s=h@40f94d5"` + ); + const retrievedState2 = getStateFromKbnUrl('_s', newUrl); + expect(retrievedState2).toEqual(state2); + }); + }); + + describe('urlControls', () => { + let history: History; + let urlControls: IKbnUrlControls; + beforeEach(() => { + history = createMemoryHistory(); + urlControls = createKbnUrlControls(history); + }); + + const getCurrentUrl = () => createPath(history.location); + it('should update url', () => { + urlControls.update('/1', false); + + expect(getCurrentUrl()).toBe('/1'); + expect(history.length).toBe(2); + + urlControls.update('/2', true); + + expect(getCurrentUrl()).toBe('/2'); + expect(history.length).toBe(2); + }); + + it('should update url async', async () => { + const pr1 = urlControls.updateAsync(() => '/1', false); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', false); + expect(getCurrentUrl()).toBe('/'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + }); + + it('should push url state if at least 1 push in async chain', async () => { + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', true); + expect(getCurrentUrl()).toBe('/'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + expect(history.length).toBe(2); + }); + + it('should replace url state if all updates in async chain are replace', async () => { + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', true); + const pr3 = urlControls.updateAsync(() => '/3', true); + expect(getCurrentUrl()).toBe('/'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + expect(history.length).toBe(1); + }); + + it('should listen for url updates', async () => { + const cb = jest.fn(); + urlControls.listen(cb); + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', true); + const pr3 = urlControls.updateAsync(() => '/3', true); + await Promise.all([pr1, pr2, pr3]); + + urlControls.update('/4', false); + urlControls.update('/5', true); + + expect(cb).toHaveBeenCalledTimes(3); + }); + + it('should flush async url updates', async () => { + const pr1 = urlControls.updateAsync(() => '/1', false); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', false); + expect(getCurrentUrl()).toBe('/'); + urlControls.flush(); + expect(getCurrentUrl()).toBe('/3'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + }); + + it('flush should take priority over regular replace behaviour', async () => { + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', true); + urlControls.flush(false); + expect(getCurrentUrl()).toBe('/3'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + expect(history.length).toBe(2); + }); + + it('should cancel async url updates', async () => { + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', true); + urlControls.cancel(); + expect(getCurrentUrl()).toBe('/'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/'); + }); + }); + + describe('getRelativeToHistoryPath', () => { + it('should extract path relative to browser history without basename', () => { + const history = createBrowserHistory(); + const url = + "http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const relativePath = getRelativeToHistoryPath(url, history); + expect(relativePath).toEqual( + "/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + }); + + it('should extract path relative to browser history with basename', () => { + const url = + "http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const history1 = createBrowserHistory({ basename: '/oxf/app/' }); + const relativePath1 = getRelativeToHistoryPath(url, history1); + expect(relativePath1).toEqual( + "/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + + const history2 = createBrowserHistory({ basename: '/oxf/app/kibana/' }); + const relativePath2 = getRelativeToHistoryPath(url, history2); + expect(relativePath2).toEqual( + "#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + }); + + it('should extract path relative to browser history with basename from relative url', () => { + const history = createBrowserHistory({ basename: '/oxf/app/' }); + const url = + "/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const relativePath = getRelativeToHistoryPath(url, history); + expect(relativePath).toEqual( + "/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + }); + + it('should extract path relative to hash history without basename', () => { + const history = createHashHistory(); + const url = + "http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const relativePath = getRelativeToHistoryPath(url, history); + expect(relativePath).toEqual( + "/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + }); + + it('should extract path relative to hash history with basename', () => { + const history = createHashHistory({ basename: 'management' }); + const url = + "http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const relativePath = getRelativeToHistoryPath(url, history); + expect(relativePath).toEqual( + "/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + }); + + it('should extract path relative to hash history with basename from relative url', () => { + const history = createHashHistory({ basename: 'management' }); + const url = + "/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const relativePath = getRelativeToHistoryPath(url, history); + expect(relativePath).toEqual( + "/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + ); + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts new file mode 100644 index 0000000000000..03c136ea3d092 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -0,0 +1,235 @@ +/* + * 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 { format as formatUrl } from 'url'; +import { createBrowserHistory, History } from 'history'; +import { decodeState, encodeState } from '../state_encoder'; +import { getCurrentUrl, parseUrl, parseUrlHash } from './parse'; +import { stringifyQueryString } from './stringify_query_string'; +import { replaceUrlHashQuery } from './format'; + +/** + * Parses a kibana url and retrieves all the states encoded into url, + * Handles both expanded rison state and hashed state (where the actual state stored in sessionStorage) + * e.g.: + * + * given an url: + * http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * will return object: + * {_a: {tab: 'indexedFields'}, _b: {f: 'test', i: '', l: ''}}; + */ +export function getStatesFromKbnUrl( + url: string = window.location.href, + keys?: string[] +): Record { + const query = parseUrlHash(url)?.query; + + if (!query) return {}; + const decoded: Record = {}; + Object.entries(query) + .filter(([key]) => (keys ? keys.includes(key) : true)) + .forEach(([q, value]) => { + decoded[q] = decodeState(value as string); + }); + + return decoded; +} + +/** + * Retrieves specific state from url by key + * e.g.: + * + * given an url: + * http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * and key '_a' + * will return object: + * {tab: 'indexedFields'} + */ +export function getStateFromKbnUrl( + key: string, + url: string = window.location.href +): State | null { + return (getStatesFromKbnUrl(url, [key])[key] as State) || null; +} + +/** + * Sets state to the url by key and returns a new url string. + * Doesn't actually updates history + * + * e.g.: + * given a url: http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * key: '_a' + * and state: {tab: 'other'} + * + * will return url: + * http://localhost:5601/oxf/app/kibana#/management/kibana/index_patterns/id?_a=(tab:other)&_b=(f:test,i:'',l:'') + */ +export function setStateToKbnUrl( + key: string, + state: State, + { useHash = false }: { useHash: boolean } = { useHash: false }, + rawUrl = window.location.href +): string { + return replaceUrlHashQuery(rawUrl, query => { + const encoded = encodeState(state, useHash); + return { + ...query, + [key]: encoded, + }; + }); +} + +/** + * A tiny wrapper around history library to listen for url changes and update url + * History library handles a bunch of cross browser edge cases + */ +export interface IKbnUrlControls { + /** + * Listen for url changes + * @param cb - get's called when url has been changed + */ + listen: (cb: () => void) => () => void; + + /** + * Updates url synchronously + * @param url - url to update to + * @param replace - use replace instead of push + */ + update: (url: string, replace: boolean) => string; + + /** + * Schedules url update to next microtask, + * Useful to batch sync changes to url to cause only one browser history update + * @param updater - fn which receives current url and should return next url to update to + * @param replace - use replace instead of push + */ + updateAsync: (updater: UrlUpdaterFnType, replace?: boolean) => Promise; + + /** + * Synchronously flushes scheduled url updates + * @param replace - if replace passed in, then uses it instead of push. Otherwise push or replace is picked depending on updateQueue + */ + flush: (replace?: boolean) => string; + + /** + * Cancels any pending url updates + */ + cancel: () => void; +} +export type UrlUpdaterFnType = (currentUrl: string) => string; + +export const createKbnUrlControls = ( + history: History = createBrowserHistory() +): IKbnUrlControls => { + const updateQueue: Array<(currentUrl: string) => string> = []; + + // if we should replace or push with next async update, + // if any call in a queue asked to push, then we should push + let shouldReplace = true; + + function updateUrl(newUrl: string, replace = false): string { + const currentUrl = getCurrentUrl(); + if (newUrl === currentUrl) return currentUrl; // skip update + + const historyPath = getRelativeToHistoryPath(newUrl, history); + + if (replace) { + history.replace(historyPath); + } else { + history.push(historyPath); + } + + return getCurrentUrl(); + } + + // queue clean up + function cleanUp() { + updateQueue.splice(0, updateQueue.length); + shouldReplace = true; + } + + // runs scheduled url updates + function flush(replace = shouldReplace) { + if (updateQueue.length === 0) return getCurrentUrl(); + const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + + cleanUp(); + + const newUrl = updateUrl(resultUrl, replace); + return newUrl; + } + + return { + listen: (cb: () => void) => + history.listen(() => { + cb(); + }), + update: (newUrl: string, replace = false) => updateUrl(newUrl, replace), + updateAsync: (updater: (currentUrl: string) => string, replace = false) => { + updateQueue.push(updater); + if (shouldReplace) { + shouldReplace = replace; + } + + // Schedule url update to the next microtask + // this allows to batch synchronous url changes + return Promise.resolve().then(() => { + return flush(); + }); + }, + flush: (replace?: boolean) => { + return flush(replace); + }, + cancel: () => { + cleanUp(); + }, + }; +}; + +/** + * Depending on history configuration extracts relative path for history updates + * 4 possible cases (see tests): + * 1. Browser history with empty base path + * 2. Browser history with base path + * 3. Hash history with empty base path + * 4. Hash history with base path + */ +export function getRelativeToHistoryPath(absoluteUrl: string, history: History): History.Path { + function stripBasename(path: string = '') { + const stripLeadingHash = (_: string) => (_.charAt(0) === '#' ? _.substr(1) : _); + const stripTrailingSlash = (_: string) => + _.charAt(_.length - 1) === '/' ? _.substr(0, _.length - 1) : _; + const baseName = stripLeadingHash(stripTrailingSlash(history.createHref({}))); + return path.startsWith(baseName) ? path.substr(baseName.length) : path; + } + const isHashHistory = history.createHref({}).includes('#'); + const parsedUrl = isHashHistory ? parseUrlHash(absoluteUrl)! : parseUrl(absoluteUrl); + const parsedHash = isHashHistory ? null : parseUrlHash(absoluteUrl); + + return formatUrl({ + pathname: stripBasename(parsedUrl.pathname), + search: stringifyQueryString(parsedUrl.query), + hash: parsedHash + ? formatUrl({ + pathname: parsedHash.pathname, + search: stringifyQueryString(parsedHash.query), + }) + : parsedUrl.hash, + }); +} diff --git a/src/plugins/kibana_utils/public/state_management/url/parse.test.ts b/src/plugins/kibana_utils/public/state_management/url/parse.test.ts new file mode 100644 index 0000000000000..774f18b734514 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/parse.test.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { parseUrlHash } from './parse'; + +describe('parseUrlHash', () => { + it('should return null if no hash', () => { + expect(parseUrlHash('http://localhost:5601/oxf/app/kibana')).toBeNull(); + }); + + it('should return parsed hash', () => { + expect(parseUrlHash('http://localhost:5601/oxf/app/kibana/#/path?test=test')).toMatchObject({ + pathname: '/path', + query: { + test: 'test', + }, + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/parse.ts b/src/plugins/kibana_utils/public/state_management/url/parse.ts new file mode 100644 index 0000000000000..95041d0662f56 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/parse.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { parse as _parseUrl } from 'url'; + +export const parseUrl = (url: string) => _parseUrl(url, true); +export const parseUrlHash = (url: string) => { + const hash = parseUrl(url).hash; + return hash ? parseUrl(hash.slice(1)) : null; +}; +export const getCurrentUrl = () => window.location.href; +export const parseCurrentUrl = () => parseUrl(getCurrentUrl()); +export const parseCurrentUrlHash = () => parseUrlHash(getCurrentUrl()); diff --git a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts b/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts new file mode 100644 index 0000000000000..3ca6cb4214682 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { encodeUriQuery, stringifyQueryString } from './stringify_query_string'; + +describe('stringifyQueryString', () => { + it('stringifyQueryString', () => { + expect( + stringifyQueryString({ + a: 'asdf1234asdf', + b: "-_.!~*'() -_.!~*'()", + c: ':@$, :@$,', + d: "&;=+# &;=+#'", + f: ' ', + g: 'null', + }) + ).toMatchInlineSnapshot( + `"a=asdf1234asdf&b=-_.!~*'()%20-_.!~*'()&c=:@$,%20:@$,&d=%26;%3D%2B%23%20%26;%3D%2B%23'&f=%20&g=null"` + ); + }); +}); + +describe('encodeUriQuery', function() { + it('should correctly encode uri query and not encode chars defined as pchar set in rfc3986', () => { + // don't encode alphanum + expect(encodeUriQuery('asdf1234asdf')).toBe('asdf1234asdf'); + + // don't encode unreserved + expect(encodeUriQuery("-_.!~*'() -_.!~*'()")).toBe("-_.!~*'()+-_.!~*'()"); + + // don't encode the rest of pchar + expect(encodeUriQuery(':@$, :@$,')).toBe(':@$,+:@$,'); + + // encode '&', ';', '=', '+', and '#' + expect(encodeUriQuery('&;=+# &;=+#')).toBe('%26;%3D%2B%23+%26;%3D%2B%23'); + + // encode ' ' as '+' + expect(encodeUriQuery(' ')).toBe('++'); + + // encode ' ' as '%20' when a flag is used + expect(encodeUriQuery(' ', true)).toBe('%20%20'); + + // do not encode `null` as '+' when flag is used + expect(encodeUriQuery('null', true)).toBe('null'); + + // do not encode `null` with no flag + expect(encodeUriQuery('null')).toBe('null'); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts b/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts new file mode 100644 index 0000000000000..e951dfac29c02 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts @@ -0,0 +1,57 @@ +/* + * 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 { stringify, ParsedUrlQuery } from 'querystring'; + +// encodeUriQuery implements the less-aggressive encoding done naturally by +// the browser. We use it to generate the same urls the browser would +export const stringifyQueryString = (query: ParsedUrlQuery) => + stringify(query, undefined, undefined, { + // encode spaces with %20 is needed to produce the same queries as angular does + // https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/src/Angular.js#L1377 + encodeURIComponent: (val: string) => encodeUriQuery(val, true), + }); + +/** + * Extracted from angular.js + * repo: https://github.com/angular/angular.js + * license: MIT - https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/LICENSE + * source: https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/src/Angular.js#L1413-L1432 + */ + +/** + * This method is intended for encoding *key* or *value* parts of query component. We need a custom + * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be + * encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ +export function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) { + return encodeURIComponent(val) + .replace(/%40/gi, '@') + .replace(/%3A/gi, ':') + .replace(/%24/g, '$') + .replace(/%2C/gi, ',') + .replace(/%3B/gi, ';') + .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); +} diff --git a/src/plugins/kibana_utils/public/state_management/url/url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/url_tracker.test.ts new file mode 100644 index 0000000000000..d7e5f99ffb700 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/url_tracker.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { createUrlTracker, IUrlTracker } from './url_tracker'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { createMemoryHistory, History } from 'history'; + +describe('urlTracker', () => { + let storage: StubBrowserStorage; + let history: History; + let urlTracker: IUrlTracker; + beforeEach(() => { + storage = new StubBrowserStorage(); + history = createMemoryHistory(); + urlTracker = createUrlTracker('test', storage); + }); + + it('should return null if no tracked url', () => { + expect(urlTracker.getTrackedUrl()).toBeNull(); + }); + + it('should return last tracked url', () => { + urlTracker.trackUrl('http://localhost:4200'); + urlTracker.trackUrl('http://localhost:4201'); + urlTracker.trackUrl('http://localhost:4202'); + expect(urlTracker.getTrackedUrl()).toBe('http://localhost:4202'); + }); + + it('should listen to history and track updates', () => { + const stop = urlTracker.startTrackingUrl(history); + expect(urlTracker.getTrackedUrl()).toBe('/'); + history.push('/1'); + history.replace('/2'); + expect(urlTracker.getTrackedUrl()).toBe('/2'); + + stop(); + history.replace('/3'); + expect(urlTracker.getTrackedUrl()).toBe('/2'); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/url_tracker.ts new file mode 100644 index 0000000000000..89e72e94ba6b4 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/url_tracker.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createBrowserHistory, History, Location } from 'history'; +import { getRelativeToHistoryPath } from './kbn_url_storage'; + +export interface IUrlTracker { + startTrackingUrl: (history?: History) => () => void; + getTrackedUrl: () => string | null; + trackUrl: (url: string) => void; +} +/** + * Replicates what src/legacy/ui/public/chrome/api/nav.ts did + * Persists the url in sessionStorage so it could be restored if navigated back to the app + */ +export function createUrlTracker(key: string, storage: Storage = sessionStorage): IUrlTracker { + return { + startTrackingUrl(history: History = createBrowserHistory()) { + const track = (location: Location) => { + const url = getRelativeToHistoryPath(history.createHref(location), history); + storage.setItem(key, url); + }; + track(history.location); + return history.listen(track); + }, + getTrackedUrl() { + return storage.getItem(key); + }, + trackUrl(url: string) { + storage.setItem(key, url); + }, + }; +} diff --git a/src/plugins/kibana_utils/public/state_sync/index.ts b/src/plugins/kibana_utils/public/state_sync/index.ts new file mode 100644 index 0000000000000..1dfa998c5bb9d --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { + createSessionStorageStateStorage, + createKbnUrlStateStorage, + IKbnUrlStateStorage, + ISessionStorageStateStorage, +} from './state_sync_state_storage'; +export { IStateSyncConfig, INullableBaseStateContainer } from './types'; +export { + syncState, + syncStates, + StopSyncStateFnType, + StartSyncStateFnType, + ISyncStateRef, +} from './state_sync'; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts new file mode 100644 index 0000000000000..cc513bc674d0f --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -0,0 +1,308 @@ +/* + * 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 { BaseStateContainer, createStateContainer } from '../state_containers'; +import { + defaultState, + pureTransitions, + TodoActions, + TodoState, +} from '../../demos/state_containers/todomvc'; +import { syncState, syncStates } from './state_sync'; +import { IStateStorage } from './state_sync_state_storage/types'; +import { Observable, Subject } from 'rxjs'; +import { + createSessionStorageStateStorage, + createKbnUrlStateStorage, + IKbnUrlStateStorage, + ISessionStorageStateStorage, +} from './state_sync_state_storage'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { createBrowserHistory, History } from 'history'; +import { INullableBaseStateContainer } from './types'; + +describe('state_sync', () => { + describe('basic', () => { + const container = createStateContainer(defaultState, pureTransitions); + beforeEach(() => { + container.set(defaultState); + }); + const storageChange$ = new Subject(); + let testStateStorage: IStateStorage; + + beforeEach(() => { + testStateStorage = { + set: jest.fn(), + get: jest.fn(), + change$: (key: string) => storageChange$.asObservable() as Observable, + }; + }); + + it('should sync state to storage', () => { + const key = '_s'; + const { start, stop } = syncState({ + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: testStateStorage, + }); + start(); + + // initial sync of state to storage is not happening + expect(testStateStorage.set).not.toBeCalled(); + + container.transitions.add({ + id: 1, + text: 'Learning transitions...', + completed: false, + }); + expect(testStateStorage.set).toBeCalledWith(key, container.getState()); + stop(); + }); + + it('should sync storage to state', () => { + const key = '_s'; + const storageState1 = [{ id: 1, text: 'todo', completed: false }]; + (testStateStorage.get as jest.Mock).mockImplementation(() => storageState1); + const { stop, start } = syncState({ + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: testStateStorage, + }); + start(); + + // initial sync of storage to state is not happening + expect(container.getState()).toEqual(defaultState); + + const storageState2 = [{ id: 1, text: 'todo', completed: true }]; + (testStateStorage.get as jest.Mock).mockImplementation(() => storageState2); + storageChange$.next(storageState2); + + expect(container.getState()).toEqual(storageState2); + + stop(); + }); + + it('should not update storage if no actual state change happened', () => { + const key = '_s'; + const { stop, start } = syncState({ + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: testStateStorage, + }); + start(); + (testStateStorage.set as jest.Mock).mockClear(); + + container.set(defaultState); + expect(testStateStorage.set).not.toBeCalled(); + + stop(); + }); + + it('should not update state container if no actual storage change happened', () => { + const key = '_s'; + const { stop, start } = syncState({ + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: testStateStorage, + }); + start(); + + const originalState = container.getState(); + const storageState = [...originalState]; + (testStateStorage.get as jest.Mock).mockImplementation(() => storageState); + storageChange$.next(storageState); + + expect(container.getState()).toBe(originalState); + + stop(); + }); + + it('storage change to null should notify state', () => { + container.set([{ completed: false, id: 1, text: 'changed' }]); + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: '_s', + stateStorage: testStateStorage, + }, + ]); + start(); + + (testStateStorage.get as jest.Mock).mockImplementation(() => null); + storageChange$.next(null); + + expect(container.getState()).toEqual(defaultState); + + stop(); + }); + }); + + describe('integration', () => { + const key = '_s'; + const container = createStateContainer(defaultState, pureTransitions); + + let sessionStorage: StubBrowserStorage; + let sessionStorageSyncStrategy: ISessionStorageStateStorage; + let history: History; + let urlSyncStrategy: IKbnUrlStateStorage; + const getCurrentUrl = () => history.createHref(history.location); + const tick = () => new Promise(resolve => setTimeout(resolve)); + + beforeEach(() => { + container.set(defaultState); + + window.location.href = '/'; + sessionStorage = new StubBrowserStorage(); + sessionStorageSyncStrategy = createSessionStorageStateStorage(sessionStorage); + history = createBrowserHistory(); + urlSyncStrategy = createKbnUrlStateStorage({ useHash: false, history }); + }); + + it('change to one storage should also update other storage', () => { + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: urlSyncStrategy, + }, + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: sessionStorageSyncStrategy, + }, + ]); + start(); + + const newStateFromUrl = [{ completed: false, id: 1, text: 'changed' }]; + history.replace('/#?_s=!((completed:!f,id:1,text:changed))'); + + expect(container.getState()).toEqual(newStateFromUrl); + expect(JSON.parse(sessionStorage.getItem(key)!)).toEqual(newStateFromUrl); + + stop(); + }); + + it('KbnUrlSyncStrategy applies url updates asynchronously to trigger single history change', async () => { + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: urlSyncStrategy, + }, + ]); + start(); + + const startHistoryLength = history.length; + container.transitions.add({ id: 2, text: '2', completed: false }); + container.transitions.add({ id: 3, text: '3', completed: false }); + container.transitions.completeAll(); + + expect(history.length).toBe(startHistoryLength); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + + await tick(); + expect(history.length).toBe(startHistoryLength + 1); + + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"` + ); + + stop(); + }); + + it('KbnUrlSyncStrategy supports flushing url updates synchronously and triggers single history change', async () => { + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: urlSyncStrategy, + }, + ]); + start(); + + const startHistoryLength = history.length; + container.transitions.add({ id: 2, text: '2', completed: false }); + container.transitions.add({ id: 3, text: '3', completed: false }); + container.transitions.completeAll(); + + expect(history.length).toBe(startHistoryLength); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + + urlSyncStrategy.flush(); + + expect(history.length).toBe(startHistoryLength + 1); + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"` + ); + + await tick(); + + expect(history.length).toBe(startHistoryLength + 1); + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"` + ); + + stop(); + }); + + it('KbnUrlSyncStrategy supports cancellation of pending updates ', async () => { + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: urlSyncStrategy, + }, + ]); + start(); + + const startHistoryLength = history.length; + container.transitions.add({ id: 2, text: '2', completed: false }); + container.transitions.add({ id: 3, text: '3', completed: false }); + container.transitions.completeAll(); + + expect(history.length).toBe(startHistoryLength); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + + urlSyncStrategy.cancel(); + + expect(history.length).toBe(startHistoryLength); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + + await tick(); + + expect(history.length).toBe(startHistoryLength); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + + stop(); + }); + }); +}); + +function withDefaultState( + stateContainer: BaseStateContainer, + // eslint-disable-next-line no-shadow + defaultState: State +): INullableBaseStateContainer { + return { + ...stateContainer, + set: (state: State | null) => { + stateContainer.set(state || defaultState); + }, + }; +} diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts new file mode 100644 index 0000000000000..f0ef1423dec71 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -0,0 +1,171 @@ +/* + * 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 { EMPTY, Subscription } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import defaultComparator from 'fast-deep-equal'; +import { IStateSyncConfig } from './types'; +import { IStateStorage } from './state_sync_state_storage'; +import { distinctUntilChangedWithInitialValue } from '../../common'; + +/** + * Utility for syncing application state wrapped in state container + * with some kind of storage (e.g. URL) + * + * Examples: + * + * 1. the simplest use case + * const stateStorage = createKbnUrlStateStorage(); + * syncState({ + * storageKey: '_s', + * stateContainer, + * stateStorage + * }); + * + * 2. conditionally configuring sync strategy + * const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) + * syncState({ + * storageKey: '_s', + * stateContainer, + * stateStorage + * }); + * + * 3. implementing custom sync strategy + * const localStorageStateStorage = { + * set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), + * get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null + * }; + * syncState({ + * storageKey: '_s', + * stateContainer, + * stateStorage: localStorageStateStorage + * }); + * + * 4. Transform state before serialising + * Useful for: + * * Migration / backward compatibility + * * Syncing part of state + * * Providing default values + * const stateToStorage = (s) => ({ tab: s.tab }); + * syncState({ + * storageKey: '_s', + * stateContainer: { + * get: () => stateToStorage(stateContainer.get()), + * set: stateContainer.set(({ tab }) => ({ ...stateContainer.get(), tab }), + * state$: stateContainer.state$.pipe(map(stateToStorage)) + * }, + * stateStorage + * }); + * + * Caveats: + * + * 1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing + * No initial sync happens when syncState() is called + */ +export type StopSyncStateFnType = () => void; +export type StartSyncStateFnType = () => void; +export interface ISyncStateRef { + // stop syncing state with storage + stop: StopSyncStateFnType; + // start syncing state with storage + start: StartSyncStateFnType; +} +export function syncState({ + storageKey, + stateStorage, + stateContainer, +}: IStateSyncConfig): ISyncStateRef { + const subscriptions: Subscription[] = []; + + const updateState = () => { + const newState = stateStorage.get(storageKey); + const oldState = stateContainer.get(); + if (!defaultComparator(newState, oldState)) { + stateContainer.set(newState); + } + }; + + const updateStorage = () => { + const newStorageState = stateContainer.get(); + const oldStorageState = stateStorage.get(storageKey); + if (!defaultComparator(newStorageState, oldStorageState)) { + stateStorage.set(storageKey, newStorageState); + } + }; + + const onStateChange$ = stateContainer.state$.pipe( + distinctUntilChangedWithInitialValue(stateContainer.get(), defaultComparator), + tap(() => updateStorage()) + ); + + const onStorageChange$ = stateStorage.change$ + ? stateStorage.change$(storageKey).pipe( + distinctUntilChangedWithInitialValue(stateStorage.get(storageKey), defaultComparator), + tap(() => { + updateState(); + }) + ) + : EMPTY; + + return { + stop: () => { + // if stateStorage has any cancellation logic, then run it + if (stateStorage.cancel) { + stateStorage.cancel(); + } + + subscriptions.forEach(s => s.unsubscribe()); + subscriptions.splice(0, subscriptions.length); + }, + start: () => { + if (subscriptions.length > 0) { + throw new Error("syncState: can't start syncing state, when syncing is in progress"); + } + subscriptions.push(onStateChange$.subscribe(), onStorageChange$.subscribe()); + }, + }; +} + +/** + * multiple different sync configs + * syncStates([ + * { + * storageKey: '_s1', + * stateStorage: stateStorage1, + * stateContainer: stateContainer1, + * }, + * { + * storageKey: '_s2', + * stateStorage: stateStorage2, + * stateContainer: stateContainer2, + * }, + * ]); + * @param stateSyncConfigs - Array of IStateSyncConfig to sync + */ +export function syncStates(stateSyncConfigs: Array>): ISyncStateRef { + const syncRefs = stateSyncConfigs.map(config => syncState(config)); + return { + stop: () => { + syncRefs.forEach(s => s.stop()); + }, + start: () => { + syncRefs.forEach(s => s.start()); + }, + }; +} diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts new file mode 100644 index 0000000000000..826122176e061 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -0,0 +1,120 @@ +/* + * 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 '../../storage/hashed_item_store/mock'; +import { createKbnUrlStateStorage, IKbnUrlStateStorage } from './create_kbn_url_state_storage'; +import { History, createBrowserHistory } from 'history'; +import { takeUntil, toArray } from 'rxjs/operators'; +import { Subject } from 'rxjs'; + +describe('KbnUrlStateStorage', () => { + describe('useHash: false', () => { + let urlStateStorage: IKbnUrlStateStorage; + let history: History; + const getCurrentUrl = () => history.createHref(history.location); + beforeEach(() => { + history = createBrowserHistory(); + history.push('/'); + urlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); + }); + + it('should persist state to url', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + await urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + }); + + it('should flush state to url', () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + urlStateStorage.flush(); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + }); + + it('should cancel url updates', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + const pr = urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + urlStateStorage.cancel(); + await pr; + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + expect(urlStateStorage.get(key)).toEqual(null); + }); + + it('should notify about url changes', async () => { + expect(urlStateStorage.change$).toBeDefined(); + const key = '_s'; + const destroy$ = new Subject(); + const result = urlStateStorage.change$!(key) + .pipe(takeUntil(destroy$), toArray()) + .toPromise(); + + history.push(`/#?${key}=(ok:1,test:test)`); + history.push(`/?query=test#?${key}=(ok:2,test:test)&some=test`); + history.push(`/?query=test#?some=test`); + + destroy$.next(); + destroy$.complete(); + + expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); + }); + }); + + describe('useHash: true', () => { + let urlStateStorage: IKbnUrlStateStorage; + let history: History; + const getCurrentUrl = () => history.createHref(history.location); + beforeEach(() => { + history = createBrowserHistory(); + history.push('/'); + urlStateStorage = createKbnUrlStateStorage({ useHash: true, history }); + }); + + it('should persist state to url', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + await urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=h@487e077"`); + expect(urlStateStorage.get(key)).toEqual(state); + }); + + it('should notify about url changes', async () => { + expect(urlStateStorage.change$).toBeDefined(); + const key = '_s'; + const destroy$ = new Subject(); + const result = urlStateStorage.change$!(key) + .pipe(takeUntil(destroy$), toArray()) + .toPromise(); + + history.push(`/#?${key}=(ok:1,test:test)`); + history.push(`/?query=test#?${key}=(ok:2,test:test)&some=test`); + history.push(`/?query=test#?some=test`); + + destroy$.next(); + destroy$.complete(); + + expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts new file mode 100644 index 0000000000000..245006349ad55 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -0,0 +1,84 @@ +/* + * 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 { Observable } from 'rxjs'; +import { map, share } from 'rxjs/operators'; +import { History } from 'history'; +import { IStateStorage } from './types'; +import { + createKbnUrlControls, + getStateFromKbnUrl, + setStateToKbnUrl, +} from '../../state_management/url'; + +export interface IKbnUrlStateStorage extends IStateStorage { + set: (key: string, state: State, opts?: { replace: boolean }) => Promise; + get: (key: string) => State | null; + change$: (key: string) => Observable; + + // cancels any pending url updates + cancel: () => void; + + // synchronously runs any pending url updates + flush: (opts?: { replace?: boolean }) => void; +} + +/** + * Implements syncing to/from url strategies. + * Replicates what was implemented in state (AppState, GlobalState) + * Both expanded and hashed use cases + */ +export const createKbnUrlStateStorage = ( + { useHash = false, history }: { useHash: boolean; history?: History } = { useHash: false } +): IKbnUrlStateStorage => { + const url = createKbnUrlControls(history); + return { + set: ( + key: string, + state: State, + { replace = false }: { replace: boolean } = { replace: false } + ) => { + // syncState() utils doesn't wait for this promise + return url.updateAsync( + currentUrl => setStateToKbnUrl(key, state, { useHash }, currentUrl), + replace + ); + }, + get: key => getStateFromKbnUrl(key), + change$: (key: string) => + new Observable(observer => { + const unlisten = url.listen(() => { + observer.next(); + }); + + return () => { + unlisten(); + }; + }).pipe( + map(() => getStateFromKbnUrl(key)), + share() + ), + flush: ({ replace = false }: { replace?: boolean } = {}) => { + url.flush(replace); + }, + cancel() { + url.cancel(); + }, + }; +}; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.test.ts new file mode 100644 index 0000000000000..f69629e755008 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + createSessionStorageStateStorage, + ISessionStorageStateStorage, +} from './create_session_storage_state_storage'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; + +describe('SessionStorageStateStorage', () => { + let browserStorage: StubBrowserStorage; + let stateStorage: ISessionStorageStateStorage; + beforeEach(() => { + browserStorage = new StubBrowserStorage(); + stateStorage = createSessionStorageStateStorage(browserStorage); + }); + + it('should synchronously sync to storage', () => { + const state = { state: 'state' }; + stateStorage.set('key', state); + expect(stateStorage.get('key')).toEqual(state); + expect(browserStorage.getItem('key')).not.toBeNull(); + }); + + it('should not implement change$', () => { + expect(stateStorage.change$).not.toBeDefined(); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts new file mode 100644 index 0000000000000..00edfdfd1ed61 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IStateStorage } from './types'; + +export interface ISessionStorageStateStorage extends IStateStorage { + set: (key: string, state: State) => void; + get: (key: string) => State | null; +} + +export const createSessionStorageStateStorage = ( + storage: Storage = window.sessionStorage +): ISessionStorageStateStorage => { + return { + set: (key: string, state: State) => storage.setItem(key, JSON.stringify(state)), + get: (key: string) => JSON.parse(storage.getItem(key)!), + }; +}; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/index.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/index.ts new file mode 100644 index 0000000000000..fe04333e5ef15 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/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 { IStateStorage } from './types'; +export { createKbnUrlStateStorage, IKbnUrlStateStorage } from './create_kbn_url_state_storage'; +export { + createSessionStorageStateStorage, + ISessionStorageStateStorage, +} from './create_session_storage_state_storage'; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts new file mode 100644 index 0000000000000..add1dc259be45 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts @@ -0,0 +1,51 @@ +/* + * 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 { Observable } from 'rxjs'; + +/** + * Any StateStorage have to implement IStateStorage interface + * StateStorage is responsible for: + * * state serialisation / deserialization + * * persisting to and retrieving from storage + * + * For an example take a look at already implemented KbnUrl state storage + */ +export interface IStateStorage { + /** + * Take in a state object, should serialise and persist + */ + set: (key: string, state: State) => any; + + /** + * Should retrieve state from the storage and deserialize it + */ + get: (key: string) => State | null; + + /** + * Should notify when the stored state has changed + */ + change$?: (key: string) => Observable; + + /** + * Optional method to cancel any pending activity + * syncState() will call it, if it is provided by IStateStorage + */ + cancel?: () => void; +} diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts new file mode 100644 index 0000000000000..0f7395ad0f0e5 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -0,0 +1,56 @@ +/* + * 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 { BaseStateContainer } from '../state_containers/types'; +import { IStateStorage } from './state_sync_state_storage'; + +export interface INullableBaseStateContainer extends BaseStateContainer { + // State container for stateSync() have to accept "null" + // for example, set() implementation could handle null and fallback to some default state + // this is required to handle edge case, when state in storage becomes empty and syncing is in progress. + // state container will be notified about about storage becoming empty with null passed in + set: (state: State | null) => void; +} + +export interface IStateSyncConfig< + State = unknown, + StateStorage extends IStateStorage = IStateStorage +> { + /** + * Storage key to use for syncing, + * e.g. storageKey '_a' should sync state to ?_a query param + */ + storageKey: string; + /** + * State container to keep in sync with storage, have to implement INullableBaseStateContainer interface + * The idea is that ./state_containers/ should be used as a state container, + * but it is also possible to implement own custom container for advanced use cases + */ + stateContainer: INullableBaseStateContainer; + /** + * State storage to use, + * State storage is responsible for serialising / deserialising and persisting / retrieving stored state + * + * There are common strategies already implemented: + * './state_sync_state_storage/' + * which replicate what State (AppState, GlobalState) in legacy world did + * + */ + stateStorage: StateStorage; +} diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 755a387afbd05..80135f1bfb6c8 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [] + "requiredPlugins": ["kibana_legacy"] } diff --git a/src/plugins/management/public/__snapshots__/management_app.test.tsx.snap b/src/plugins/management/public/__snapshots__/management_app.test.tsx.snap new file mode 100644 index 0000000000000..7f13472ee02ee --- /dev/null +++ b/src/plugins/management/public/__snapshots__/management_app.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Management app can mount and unmount 1`] = ` +
+
+ Test App - Hello world! +
+
+`; + +exports[`Management app can mount and unmount 2`] = `
`; diff --git a/src/plugins/management/public/components/_index.scss b/src/plugins/management/public/components/_index.scss new file mode 100644 index 0000000000000..df0ebb48803d9 --- /dev/null +++ b/src/plugins/management/public/components/_index.scss @@ -0,0 +1 @@ +@import './management_sidebar_nav/index'; diff --git a/src/legacy/ui/public/utils/parse_interval.d.ts b/src/plugins/management/public/components/index.ts similarity index 86% rename from src/legacy/ui/public/utils/parse_interval.d.ts rename to src/plugins/management/public/components/index.ts index 9d78b4ef6cddf..2650d23d3c25c 100644 --- a/src/legacy/ui/public/utils/parse_interval.d.ts +++ b/src/plugins/management/public/components/index.ts @@ -17,6 +17,5 @@ * under the License. */ -import moment from 'moment'; - -export function parseInterval(interval: string): moment.Duration | null; +export { ManagementSidebarNav } from './management_sidebar_nav'; +export { ManagementChrome } from './management_chrome'; diff --git a/src/legacy/core_plugins/kbn_doc_views/public/kbn_doc_views.js b/src/plugins/management/public/components/management_chrome/index.ts similarity index 93% rename from src/legacy/core_plugins/kbn_doc_views/public/kbn_doc_views.js rename to src/plugins/management/public/components/management_chrome/index.ts index 2608e60e70742..b82c1af871be7 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/kbn_doc_views.js +++ b/src/plugins/management/public/components/management_chrome/index.ts @@ -17,5 +17,4 @@ * under the License. */ -import './views/table'; -import './views/json'; +export { ManagementChrome } from './management_chrome'; diff --git a/src/plugins/management/public/components/management_chrome/management_chrome.tsx b/src/plugins/management/public/components/management_chrome/management_chrome.tsx new file mode 100644 index 0000000000000..df844e2208936 --- /dev/null +++ b/src/plugins/management/public/components/management_chrome/management_chrome.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as React from 'react'; +import { EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ManagementSidebarNav } from '../management_sidebar_nav'; +import { LegacySection } from '../../types'; +import { ManagementSection } from '../../management_section'; + +interface Props { + getSections: () => ManagementSection[]; + legacySections: LegacySection[]; + selectedId: string; + onMounted: (element: HTMLDivElement) => void; +} + +export class ManagementChrome extends React.Component { + private container = React.createRef(); + componentDidMount() { + if (this.container.current) { + this.props.onMounted(this.container.current); + } + } + render() { + return ( + + + + + + +
+ + + + ); + } +} diff --git a/src/plugins/management/public/components/management_sidebar_nav/__snapshots__/management_sidebar_nav.test.ts.snap b/src/plugins/management/public/components/management_sidebar_nav/__snapshots__/management_sidebar_nav.test.ts.snap new file mode 100644 index 0000000000000..e7225b356ed68 --- /dev/null +++ b/src/plugins/management/public/components/management_sidebar_nav/__snapshots__/management_sidebar_nav.test.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Management adds legacy apps to existing SidebarNav sections 1`] = ` +Array [ + Object { + "data-test-subj": "activeSection", + "icon": null, + "id": "activeSection", + "items": Array [ + Object { + "data-test-subj": "item", + "href": undefined, + "id": "item", + "isSelected": false, + "name": "item", + "order": undefined, + }, + ], + "name": "activeSection", + "order": 10, + }, + Object { + "data-test-subj": "no-active-items", + "icon": null, + "id": "no-active-items", + "items": Array [ + Object { + "data-test-subj": "disabled", + "href": undefined, + "id": "disabled", + "isSelected": false, + "name": "disabled", + "order": undefined, + }, + Object { + "data-test-subj": "notVisible", + "href": undefined, + "id": "notVisible", + "isSelected": false, + "name": "notVisible", + "order": undefined, + }, + ], + "name": "No active items", + "order": 10, + }, +] +`; + +exports[`Management maps legacy sections and apps into SidebarNav items 1`] = ` +Array [ + Object { + "data-test-subj": "no-active-items", + "icon": null, + "id": "no-active-items", + "items": Array [ + Object { + "data-test-subj": "disabled", + "href": undefined, + "id": "disabled", + "isSelected": false, + "name": "disabled", + "order": undefined, + }, + Object { + "data-test-subj": "notVisible", + "href": undefined, + "id": "notVisible", + "isSelected": false, + "name": "notVisible", + "order": undefined, + }, + ], + "name": "No active items", + "order": 10, + }, + Object { + "data-test-subj": "activeSection", + "icon": null, + "id": "activeSection", + "items": Array [ + Object { + "data-test-subj": "item", + "href": undefined, + "id": "item", + "isSelected": false, + "name": "item", + "order": undefined, + }, + ], + "name": "activeSection", + "order": 10, + }, +] +`; diff --git a/src/legacy/ui/public/management/components/_index.scss b/src/plugins/management/public/components/management_sidebar_nav/_index.scss similarity index 100% rename from src/legacy/ui/public/management/components/_index.scss rename to src/plugins/management/public/components/management_sidebar_nav/_index.scss diff --git a/src/legacy/ui/public/management/components/_sidebar_nav.scss b/src/plugins/management/public/components/management_sidebar_nav/_sidebar_nav.scss similarity index 88% rename from src/legacy/ui/public/management/components/_sidebar_nav.scss rename to src/plugins/management/public/components/management_sidebar_nav/_sidebar_nav.scss index 0c2b2bc228b2c..cf88ed9b0a88b 100644 --- a/src/legacy/ui/public/management/components/_sidebar_nav.scss +++ b/src/plugins/management/public/components/management_sidebar_nav/_sidebar_nav.scss @@ -1,4 +1,4 @@ -.mgtSidebarNav { +.mgtSideBarNav { width: 192px; } diff --git a/src/plugins/management/public/components/management_sidebar_nav/index.ts b/src/plugins/management/public/components/management_sidebar_nav/index.ts new file mode 100644 index 0000000000000..79142fdb69a74 --- /dev/null +++ b/src/plugins/management/public/components/management_sidebar_nav/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 { ManagementSidebarNav } from './management_sidebar_nav'; diff --git a/src/legacy/ui/public/management/components/sidebar_nav.test.ts b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.test.ts similarity index 75% rename from src/legacy/ui/public/management/components/sidebar_nav.test.ts rename to src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.test.ts index e02cc7d2901b6..e04e0a7572612 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.test.ts +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { IndexedArray } from '../../indexed_array'; -import { sideNavItems } from '../components/sidebar_nav'; +import { IndexedArray } from '../../../../../legacy/ui/public/indexed_array'; +import { mergeLegacyItems } from './management_sidebar_nav'; const toIndexedArray = (initialSet: any[]) => new IndexedArray({ @@ -30,30 +30,33 @@ const toIndexedArray = (initialSet: any[]) => const activeProps = { visible: true, disabled: false }; const disabledProps = { visible: true, disabled: true }; const notVisibleProps = { visible: false, disabled: false }; - const visibleItem = { display: 'item', id: 'item', ...activeProps }; const notVisibleSection = { display: 'Not visible', id: 'not-visible', + order: 10, visibleItems: toIndexedArray([visibleItem]), ...notVisibleProps, }; const disabledSection = { display: 'Disabled', id: 'disabled', + order: 10, visibleItems: toIndexedArray([visibleItem]), ...disabledProps, }; const noItemsSection = { display: 'No items', id: 'no-items', + order: 10, visibleItems: toIndexedArray([]), ...activeProps, }; const noActiveItemsSection = { display: 'No active items', id: 'no-active-items', + order: 10, visibleItems: toIndexedArray([ { display: 'disabled', id: 'disabled', ...disabledProps }, { display: 'notVisible', id: 'notVisible', ...notVisibleProps }, @@ -63,6 +66,7 @@ const noActiveItemsSection = { const activeSection = { display: 'activeSection', id: 'activeSection', + order: 10, visibleItems: toIndexedArray([visibleItem]), ...activeProps, }; @@ -76,7 +80,19 @@ const managementSections = [ ]; describe('Management', () => { - it('filters and filters and maps section objects into SidebarNav items', () => { - expect(sideNavItems(managementSections, 'active-item-id')).toMatchSnapshot(); + it('maps legacy sections and apps into SidebarNav items', () => { + expect(mergeLegacyItems([], managementSections, 'active-item-id')).toMatchSnapshot(); + }); + + it('adds legacy apps to existing SidebarNav sections', () => { + const navSection = { + 'data-test-subj': 'activeSection', + icon: null, + id: 'activeSection', + items: [], + name: 'activeSection', + order: 10, + }; + expect(mergeLegacyItems([navSection], managementSections, 'active-item-id')).toMatchSnapshot(); }); }); diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx new file mode 100644 index 0000000000000..cb0b82d0f0bde --- /dev/null +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -0,0 +1,200 @@ +/* + * 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 { + EuiIcon, + // @ts-ignore + EuiSideNav, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { LegacySection, LegacyApp } from '../../types'; +import { ManagementApp } from '../../management_app'; +import { ManagementSection } from '../../management_section'; + +interface NavApp { + id: string; + name: string; + [key: string]: unknown; + order: number; // only needed while merging platform and legacy +} + +interface NavSection extends NavApp { + items: NavApp[]; +} + +interface ManagementSidebarNavProps { + getSections: () => ManagementSection[]; + legacySections: LegacySection[]; + selectedId: string; +} + +interface ManagementSidebarNavState { + isSideNavOpenOnMobile: boolean; +} + +const managementSectionOrAppToNav = (appOrSection: ManagementApp | ManagementSection) => ({ + id: appOrSection.id, + name: appOrSection.title, + 'data-test-subj': appOrSection.id, + order: appOrSection.order, +}); + +const managementSectionToNavSection = (section: ManagementSection) => { + const iconType = section.euiIconType + ? section.euiIconType + : section.icon + ? section.icon + : 'empty'; + + return { + icon: , + ...managementSectionOrAppToNav(section), + }; +}; + +const managementAppToNavItem = (selectedId?: string, parentId?: string) => ( + app: ManagementApp +) => ({ + isSelected: selectedId === app.id, + href: `#/management/${parentId}/${app.id}`, + ...managementSectionOrAppToNav(app), +}); + +const legacySectionToNavSection = (section: LegacySection) => ({ + name: section.display, + id: section.id, + icon: section.icon ? : null, + items: [], + 'data-test-subj': section.id, + // @ts-ignore + order: section.order, +}); + +const legacyAppToNavItem = (app: LegacyApp, selectedId: string) => ({ + isSelected: selectedId === app.id, + name: app.display, + id: app.id, + href: app.url, + 'data-test-subj': app.id, + // @ts-ignore + order: app.order, +}); + +const sectionVisible = (section: LegacySection | LegacyApp) => !section.disabled && section.visible; + +const sideNavItems = (sections: ManagementSection[], selectedId: string) => + sections.map(section => ({ + items: section.getAppsEnabled().map(managementAppToNavItem(selectedId, section.id)), + ...managementSectionToNavSection(section), + })); + +const findOrAddSection = (navItems: NavSection[], legacySection: LegacySection): NavSection => { + const foundSection = navItems.find(sec => sec.id === legacySection.id); + + if (foundSection) { + return foundSection; + } else { + const newSection = legacySectionToNavSection(legacySection); + navItems.push(newSection); + navItems.sort((a: NavSection, b: NavSection) => a.order - b.order); // only needed while merging platform and legacy + return newSection; + } +}; + +export const mergeLegacyItems = ( + navItems: NavSection[], + legacySections: LegacySection[], + selectedId: string +) => { + const filteredLegacySections = legacySections + .filter(sectionVisible) + .filter(section => section.visibleItems.length); + + filteredLegacySections.forEach(legacySection => { + const section = findOrAddSection(navItems, legacySection); + legacySection.visibleItems.forEach(app => { + section.items.push(legacyAppToNavItem(app, selectedId)); + return section.items.sort((a, b) => a.order - b.order); + }); + }); + + return navItems; +}; + +const sectionsToItems = ( + sections: ManagementSection[], + legacySections: LegacySection[], + selectedId: string +) => { + const navItems = sideNavItems(sections, selectedId); + return mergeLegacyItems(navItems, legacySections, selectedId); +}; + +export class ManagementSidebarNav extends React.Component< + ManagementSidebarNavProps, + ManagementSidebarNavState +> { + constructor(props: ManagementSidebarNavProps) { + super(props); + this.state = { + isSideNavOpenOnMobile: false, + }; + } + + public render() { + const HEADER_ID = 'management-nav-header'; + + return ( + <> + +

+ {i18n.translate('management.nav.label', { + defaultMessage: 'Management', + })} +

+
+ + + ); + } + + private renderMobileTitle() { + return ; + } + + private toggleOpenOnMobile = () => { + this.setState({ + isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile, + }); + }; +} diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index ee3866c734f19..faec466dbd671 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -24,4 +24,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new ManagementPlugin(); } -export { ManagementStart } from './types'; +export { ManagementSetup, ManagementStart, RegisterManagementApp } from './types'; +export { ManagementApp } from './management_app'; +export { ManagementSection } from './management_section'; +export { ManagementSidebarNav } from './components'; // for use in legacy management apps diff --git a/src/plugins/management/public/legacy/index.js b/src/plugins/management/public/legacy/index.js index 63b9d2c6b27d7..f2e0ba89b7b59 100644 --- a/src/plugins/management/public/legacy/index.js +++ b/src/plugins/management/public/legacy/index.js @@ -17,4 +17,5 @@ * under the License. */ -export { management } from './sections_register'; +export { LegacyManagementAdapter } from './sections_register'; +export { LegacyManagementSection } from './section'; diff --git a/src/plugins/management/public/legacy/section.js b/src/plugins/management/public/legacy/section.js index f269e3fe295b7..7d733b7b3173b 100644 --- a/src/plugins/management/public/legacy/section.js +++ b/src/plugins/management/public/legacy/section.js @@ -22,7 +22,7 @@ import { IndexedArray } from '../../../../legacy/ui/public/indexed_array'; const listeners = []; -export class ManagementSection { +export class LegacyManagementSection { /** * @param {string} id * @param {object} options @@ -83,7 +83,11 @@ export class ManagementSection { */ register(id, options = {}) { - const item = new ManagementSection(id, assign(options, { parent: this }), this.capabilities); + const item = new LegacyManagementSection( + id, + assign(options, { parent: this }), + this.capabilities + ); if (this.hasItem(id)) { throw new Error(`'${id}' is already registered`); diff --git a/src/plugins/management/public/legacy/section.test.js b/src/plugins/management/public/legacy/section.test.js index 61bafd298afb3..45cc80ef80edd 100644 --- a/src/plugins/management/public/legacy/section.test.js +++ b/src/plugins/management/public/legacy/section.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { ManagementSection } from './section'; +import { LegacyManagementSection } from './section'; import { IndexedArray } from '../../../../legacy/ui/public/indexed_array'; const capabilitiesMock = { @@ -29,42 +29,42 @@ const capabilitiesMock = { describe('ManagementSection', () => { describe('constructor', () => { it('defaults display to id', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.display).toBe('kibana'); }); it('defaults visible to true', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.visible).toBe(true); }); it('defaults disabled to false', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.disabled).toBe(false); }); it('defaults tooltip to empty string', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.tooltip).toBe(''); }); it('defaults url to empty string', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.url).toBe(''); }); it('exposes items', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.items).toHaveLength(0); }); it('exposes visibleItems', () => { - const section = new ManagementSection('kibana', {}, capabilitiesMock); + const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); expect(section.visibleItems).toHaveLength(0); }); it('assigns all options', () => { - const section = new ManagementSection( + const section = new LegacyManagementSection( 'kibana', { description: 'test', url: 'foobar' }, capabilitiesMock @@ -78,11 +78,11 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); }); it('returns a ManagementSection', () => { - expect(section.register('about')).toBeInstanceOf(ManagementSection); + expect(section.register('about')).toBeInstanceOf(LegacyManagementSection); }); it('provides a reference to the parent', () => { @@ -93,7 +93,7 @@ describe('ManagementSection', () => { section.register('about', { description: 'test' }); expect(section.items).toHaveLength(1); - expect(section.items[0]).toBeInstanceOf(ManagementSection); + expect(section.items[0]).toBeInstanceOf(LegacyManagementSection); expect(section.items[0].id).toBe('about'); }); @@ -126,7 +126,7 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); section.register('about'); }); @@ -157,12 +157,12 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); section.register('about'); }); it('returns registered section', () => { - expect(section.getSection('about')).toBeInstanceOf(ManagementSection); + expect(section.getSection('about')).toBeInstanceOf(LegacyManagementSection); }); it('returns undefined if un-registered', () => { @@ -171,7 +171,7 @@ describe('ManagementSection', () => { it('returns sub-sections specified via a /-separated path', () => { section.getSection('about').register('time'); - expect(section.getSection('about/time')).toBeInstanceOf(ManagementSection); + expect(section.getSection('about/time')).toBeInstanceOf(LegacyManagementSection); expect(section.getSection('about/time')).toBe(section.getSection('about').getSection('time')); }); @@ -184,7 +184,7 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); section.register('three', { order: 3 }); section.register('one', { order: 1 }); @@ -214,7 +214,7 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); }); it('hide sets visible to false', () => { @@ -233,7 +233,7 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); }); it('disable sets disabled to true', () => { @@ -251,7 +251,7 @@ describe('ManagementSection', () => { let section; beforeEach(() => { - section = new ManagementSection('kibana', {}, capabilitiesMock); + section = new LegacyManagementSection('kibana', {}, capabilitiesMock); section.register('three', { order: 3 }); section.register('one', { order: 1 }); diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index 888b2c5bc3aeb..63d919377f89e 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -17,44 +17,48 @@ * under the License. */ -import { ManagementSection } from './section'; +import { LegacyManagementSection } from './section'; import { i18n } from '@kbn/i18n'; -export const management = capabilities => { - const main = new ManagementSection( - 'management', - { - display: i18n.translate('management.displayName', { - defaultMessage: 'Management', - }), - }, - capabilities - ); +export class LegacyManagementAdapter { + main = undefined; + init = capabilities => { + this.main = new LegacyManagementSection( + 'management', + { + display: i18n.translate('management.displayName', { + defaultMessage: 'Management', + }), + }, + capabilities + ); - main.register('data', { - display: i18n.translate('management.connectDataDisplayName', { - defaultMessage: 'Connect Data', - }), - order: 0, - }); + this.main.register('data', { + display: i18n.translate('management.connectDataDisplayName', { + defaultMessage: 'Connect Data', + }), + order: 0, + }); - main.register('elasticsearch', { - display: 'Elasticsearch', - order: 20, - icon: 'logoElasticsearch', - }); + this.main.register('elasticsearch', { + display: 'Elasticsearch', + order: 20, + icon: 'logoElasticsearch', + }); - main.register('kibana', { - display: 'Kibana', - order: 30, - icon: 'logoKibana', - }); + this.main.register('kibana', { + display: 'Kibana', + order: 30, + icon: 'logoKibana', + }); - main.register('logstash', { - display: 'Logstash', - order: 30, - icon: 'logoLogstash', - }); + this.main.register('logstash', { + display: 'Logstash', + order: 30, + icon: 'logoLogstash', + }); - return main; -}; + return this.main; + }; + getManagement = () => this.main; +} diff --git a/src/plugins/management/public/management_app.test.tsx b/src/plugins/management/public/management_app.test.tsx new file mode 100644 index 0000000000000..a76b234d95ef5 --- /dev/null +++ b/src/plugins/management/public/management_app.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { coreMock } from '../../../core/public/mocks'; + +import { ManagementApp } from './management_app'; +// @ts-ignore +import { LegacyManagementSection } from './legacy'; + +function createTestApp() { + const legacySection = new LegacyManagementSection('legacy'); + return new ManagementApp( + { + id: 'test-app', + title: 'Test App', + basePath: '', + mount(params) { + params.setBreadcrumbs([{ text: 'Test App' }]); + ReactDOM.render(
Test App - Hello world!
, params.element); + + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }, + () => [], + jest.fn(), + () => legacySection, + coreMock.createSetup().getStartServices + ); +} + +test('Management app can mount and unmount', async () => { + const testApp = createTestApp(); + const container = document.createElement('div'); + document.body.appendChild(container); + const unmount = testApp.mount({ element: container, basePath: '', setBreadcrumbs: jest.fn() }); + expect(container).toMatchSnapshot(); + (await unmount)(); + expect(container).toMatchSnapshot(); +}); + +test('Enabled by default, can disable', () => { + const testApp = createTestApp(); + expect(testApp.enabled).toBe(true); + testApp.disable(); + expect(testApp.enabled).toBe(false); +}); diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx new file mode 100644 index 0000000000000..f7e8dba4f8210 --- /dev/null +++ b/src/plugins/management/public/management_app.tsx @@ -0,0 +1,102 @@ +/* + * 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 * as React from 'react'; +import ReactDOM from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { CreateManagementApp, ManagementSectionMount, Unmount } from './types'; +import { KibanaLegacySetup } from '../../kibana_legacy/public'; +// @ts-ignore +import { LegacyManagementSection } from './legacy'; +import { ManagementChrome } from './components'; +import { ManagementSection } from './management_section'; +import { ChromeBreadcrumb, CoreSetup } from '../../../core/public/'; + +export class ManagementApp { + readonly id: string; + readonly title: string; + readonly basePath: string; + readonly order: number; + readonly mount: ManagementSectionMount; + protected enabledStatus: boolean = true; + + constructor( + { id, title, basePath, order = 100, mount }: CreateManagementApp, + getSections: () => ManagementSection[], + registerLegacyApp: KibanaLegacySetup['registerLegacyApp'], + getLegacyManagementSections: () => LegacyManagementSection, + getStartServices: CoreSetup['getStartServices'] + ) { + this.id = id; + this.title = title; + this.basePath = basePath; + this.order = order; + this.mount = mount; + + registerLegacyApp({ + id: basePath.substr(1), // get rid of initial slash + title, + mount: async ({}, params) => { + let appUnmount: Unmount; + async function setBreadcrumbs(crumbs: ChromeBreadcrumb[]) { + const [coreStart] = await getStartServices(); + coreStart.chrome.setBreadcrumbs([ + { + text: i18n.translate('management.breadcrumb', { + defaultMessage: 'Management', + }), + href: '#/management', + }, + ...crumbs, + ]); + } + + ReactDOM.render( + { + appUnmount = await mount({ + basePath, + element, + setBreadcrumbs, + }); + }} + />, + params.element + ); + + return async () => { + appUnmount(); + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }); + } + public enable() { + this.enabledStatus = true; + } + public disable() { + this.enabledStatus = false; + } + public get enabled() { + return this.enabledStatus; + } +} diff --git a/src/plugins/management/public/management_section.test.ts b/src/plugins/management/public/management_section.test.ts new file mode 100644 index 0000000000000..c68175ee0a678 --- /dev/null +++ b/src/plugins/management/public/management_section.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { ManagementSection } from './management_section'; +// @ts-ignore +import { LegacyManagementSection } from './legacy'; +import { coreMock } from '../../../core/public/mocks'; + +function createSection(registerLegacyApp: () => void) { + const legacySection = new LegacyManagementSection('legacy'); + const getLegacySection = () => legacySection; + const getManagementSections: () => ManagementSection[] = () => []; + + const testSectionConfig = { id: 'test-section', title: 'Test Section' }; + return new ManagementSection( + testSectionConfig, + getManagementSections, + registerLegacyApp, + getLegacySection, + coreMock.createSetup().getStartServices + ); +} + +test('cannot register two apps with the same id', () => { + const registerLegacyApp = jest.fn(); + const section = createSection(registerLegacyApp); + + const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} }; + + section.registerApp(testAppConfig); + expect(registerLegacyApp).toHaveBeenCalled(); + expect(section.apps.length).toEqual(1); + + expect(() => { + section.registerApp(testAppConfig); + }).toThrow(); +}); + +test('can enable and disable apps', () => { + const registerLegacyApp = jest.fn(); + const section = createSection(registerLegacyApp); + + const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} }; + + const app = section.registerApp(testAppConfig); + expect(section.getAppsEnabled().length).toEqual(1); + app.disable(); + expect(section.getAppsEnabled().length).toEqual(0); +}); diff --git a/src/plugins/management/public/management_section.ts b/src/plugins/management/public/management_section.ts new file mode 100644 index 0000000000000..2f323c4b6a9cf --- /dev/null +++ b/src/plugins/management/public/management_section.ts @@ -0,0 +1,78 @@ +/* + * 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 { CreateSection, RegisterManagementAppArgs } from './types'; +import { KibanaLegacySetup } from '../../kibana_legacy/public'; +import { CoreSetup } from '../../../core/public'; +// @ts-ignore +import { LegacyManagementSection } from './legacy'; +import { ManagementApp } from './management_app'; + +export class ManagementSection { + public readonly id: string = ''; + public readonly title: string = ''; + public readonly apps: ManagementApp[] = []; + public readonly order: number; + public readonly euiIconType?: string; + public readonly icon?: string; + private readonly getSections: () => ManagementSection[]; + private readonly registerLegacyApp: KibanaLegacySetup['registerLegacyApp']; + private readonly getLegacyManagementSection: () => LegacyManagementSection; + private readonly getStartServices: CoreSetup['getStartServices']; + + constructor( + { id, title, order = 100, euiIconType, icon }: CreateSection, + getSections: () => ManagementSection[], + registerLegacyApp: KibanaLegacySetup['registerLegacyApp'], + getLegacyManagementSection: () => ManagementSection, + getStartServices: CoreSetup['getStartServices'] + ) { + this.id = id; + this.title = title; + this.order = order; + this.euiIconType = euiIconType; + this.icon = icon; + this.getSections = getSections; + this.registerLegacyApp = registerLegacyApp; + this.getLegacyManagementSection = getLegacyManagementSection; + this.getStartServices = getStartServices; + } + + registerApp({ id, title, order, mount }: RegisterManagementAppArgs) { + if (this.getApp(id)) { + throw new Error(`Management app already registered - id: ${id}, title: ${title}`); + } + + const app = new ManagementApp( + { id, title, order, mount, basePath: `/management/${this.id}/${id}` }, + this.getSections, + this.registerLegacyApp, + this.getLegacyManagementSection, + this.getStartServices + ); + this.apps.push(app); + return app; + } + getApp(id: ManagementApp['id']) { + return this.apps.find(app => app.id === id); + } + getAppsEnabled() { + return this.apps.filter(app => app.enabled).sort((a, b) => a.order - b.order); + } +} diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts new file mode 100644 index 0000000000000..854406a10335b --- /dev/null +++ b/src/plugins/management/public/management_service.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { ManagementService } from './management_service'; +import { coreMock } from '../../../core/public/mocks'; + +const mockKibanaLegacy = { registerLegacyApp: () => {}, forwardApp: () => {} }; + +test('Provides default sections', () => { + const service = new ManagementService().setup( + mockKibanaLegacy, + () => {}, + coreMock.createSetup().getStartServices + ); + expect(service.getAllSections().length).toEqual(3); + expect(service.getSection('kibana')).not.toBeUndefined(); + expect(service.getSection('logstash')).not.toBeUndefined(); + expect(service.getSection('elasticsearch')).not.toBeUndefined(); +}); + +test('Register section, enable and disable', () => { + const service = new ManagementService().setup( + mockKibanaLegacy, + () => {}, + coreMock.createSetup().getStartServices + ); + const testSection = service.register({ id: 'test-section', title: 'Test Section' }); + expect(service.getSection('test-section')).not.toBeUndefined(); + + const testApp = testSection.registerApp({ + id: 'test-app', + title: 'Test App', + mount: () => () => {}, + }); + expect(testSection.getApp('test-app')).not.toBeUndefined(); + expect(service.getSectionsEnabled().length).toEqual(1); + testApp.disable(); + expect(service.getSectionsEnabled().length).toEqual(0); +}); diff --git a/src/plugins/management/public/management_service.ts b/src/plugins/management/public/management_service.ts new file mode 100644 index 0000000000000..4a900345b3843 --- /dev/null +++ b/src/plugins/management/public/management_service.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ManagementSection } from './management_section'; +import { KibanaLegacySetup } from '../../kibana_legacy/public'; +// @ts-ignore +import { LegacyManagementSection } from './legacy'; +import { CreateSection } from './types'; +import { CoreSetup, CoreStart } from '../../../core/public'; + +export class ManagementService { + private sections: ManagementSection[] = []; + + private register( + registerLegacyApp: KibanaLegacySetup['registerLegacyApp'], + getLegacyManagement: () => LegacyManagementSection, + getStartServices: CoreSetup['getStartServices'] + ) { + return (section: CreateSection) => { + if (this.getSection(section.id)) { + throw Error(`ManagementSection '${section.id}' already registered`); + } + + const newSection = new ManagementSection( + section, + this.getSectionsEnabled.bind(this), + registerLegacyApp, + getLegacyManagement, + getStartServices + ); + this.sections.push(newSection); + return newSection; + }; + } + private getSection(sectionId: ManagementSection['id']) { + return this.sections.find(section => section.id === sectionId); + } + + private getAllSections() { + return this.sections; + } + + private getSectionsEnabled() { + return this.sections + .filter(section => section.getAppsEnabled().length > 0) + .sort((a, b) => a.order - b.order); + } + + private sharedInterface = { + getSection: this.getSection.bind(this), + getSectionsEnabled: this.getSectionsEnabled.bind(this), + getAllSections: this.getAllSections.bind(this), + }; + + public setup( + kibanaLegacy: KibanaLegacySetup, + getLegacyManagement: () => LegacyManagementSection, + getStartServices: CoreSetup['getStartServices'] + ) { + const register = this.register.bind(this)( + kibanaLegacy.registerLegacyApp, + getLegacyManagement, + getStartServices + ); + + register({ id: 'kibana', title: 'Kibana', order: 30, euiIconType: 'logoKibana' }); + register({ id: 'logstash', title: 'Logstash', order: 30, euiIconType: 'logoLogstash' }); + register({ + id: 'elasticsearch', + title: 'Elasticsearch', + order: 20, + euiIconType: 'logoElasticsearch', + }); + + return { + register, + ...this.sharedInterface, + }; + } + + public start(navigateToApp: CoreStart['application']['navigateToApp']) { + return { + navigateToApp, // apps are currently registered as top level apps but this may change in the future + ...this.sharedInterface, + }; + } +} diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index c65dfd1dc7bb4..195d96c11d8d9 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -18,18 +18,30 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { ManagementStart } from './types'; +import { ManagementSetup, ManagementStart } from './types'; +import { ManagementService } from './management_service'; +import { KibanaLegacySetup } from '../../kibana_legacy/public'; // @ts-ignore -import { management } from './legacy'; +import { LegacyManagementAdapter } from './legacy'; -export class ManagementPlugin implements Plugin<{}, ManagementStart> { - public setup(core: CoreSetup) { - return {}; +export class ManagementPlugin implements Plugin { + private managementSections = new ManagementService(); + private legacyManagement = new LegacyManagementAdapter(); + + public setup(core: CoreSetup, { kibana_legacy }: { kibana_legacy: KibanaLegacySetup }) { + return { + sections: this.managementSections.setup( + kibana_legacy, + this.legacyManagement.getManagement, + core.getStartServices + ), + }; } public start(core: CoreStart) { return { - legacy: management(core.application.capabilities), + sections: this.managementSections.start(core.application.navigateToApp), + legacy: this.legacyManagement.init(core.application.capabilities), }; } } diff --git a/src/plugins/management/public/types.ts b/src/plugins/management/public/types.ts index 6ca1faf338c39..4dbea30ff062d 100644 --- a/src/plugins/management/public/types.ts +++ b/src/plugins/management/public/types.ts @@ -17,6 +17,82 @@ * under the License. */ +import { IconType } from '@elastic/eui'; +import { ManagementApp } from './management_app'; +import { ManagementSection } from './management_section'; +import { ChromeBreadcrumb, ApplicationStart } from '../../../core/public/'; + +export interface ManagementSetup { + sections: SectionsServiceSetup; +} + export interface ManagementStart { + sections: SectionsServiceStart; legacy: any; } + +interface SectionsServiceSetup { + getSection: (sectionId: ManagementSection['id']) => ManagementSection | undefined; + getAllSections: () => ManagementSection[]; + register: RegisterSection; +} + +interface SectionsServiceStart { + getSection: (sectionId: ManagementSection['id']) => ManagementSection | undefined; + getAllSections: () => ManagementSection[]; + navigateToApp: ApplicationStart['navigateToApp']; +} + +export interface CreateSection { + id: string; + title: string; + order?: number; + euiIconType?: string; // takes precedence over `icon` property. + icon?: string; // URL to image file; fallback if no `euiIconType` +} + +export type RegisterSection = (section: CreateSection) => ManagementSection; + +export interface RegisterManagementAppArgs { + id: string; + title: string; + mount: ManagementSectionMount; + order?: number; +} + +export type RegisterManagementApp = (managementApp: RegisterManagementAppArgs) => ManagementApp; + +export type Unmount = () => Promise | void; + +interface ManagementAppMountParams { + basePath: string; // base path for setting up your router + element: HTMLElement; // element the section should render into + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +} + +export type ManagementSectionMount = ( + params: ManagementAppMountParams +) => Unmount | Promise; + +export interface CreateManagementApp { + id: string; + title: string; + basePath: string; + order?: number; + mount: ManagementSectionMount; +} + +export interface LegacySection extends LegacyApp { + visibleItems: LegacyApp[]; +} + +export interface LegacyApp { + disabled: boolean; + visible: boolean; + id: string; + display: string; + url?: string; + euiIconType?: IconType; + icon?: string; + order: number; +} diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 586254603567b..4873fe0926472 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { map, mergeMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { schema, TypeOf } from '@kbn/config-schema'; import { @@ -108,9 +108,7 @@ class Plugin { return `Some exposed data derived from config: ${configValue.secret}`; }) ), - pingElasticsearch$: core.elasticsearch.adminClient$.pipe( - mergeMap(client => client.callAsInternalUser('ping')) - ), + pingElasticsearch: () => core.elasticsearch.adminClient.callAsInternalUser('ping'), }; } diff --git a/src/test_utils/public/simulate_keys.js b/src/test_utils/public/simulate_keys.js index 8ee4a79e6bc7c..a876d67325c05 100644 --- a/src/test_utils/public/simulate_keys.js +++ b/src/test_utils/public/simulate_keys.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import _ from 'lodash'; import Bluebird from 'bluebird'; -import { keyMap } from 'ui/utils/key_map'; +import { keyMap } from 'ui/directives/key_map'; const reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); /** diff --git a/tasks/config/karma.js b/tasks/config/karma.js index c0d6074da61c5..0acd452530b30 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -20,6 +20,7 @@ import { dirname } from 'path'; import { times } from 'lodash'; import { makeJunitReportPath } from '@kbn/test'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); @@ -48,6 +49,25 @@ module.exports = function(grunt) { return ['progress']; } + function getKarmaFiles(shardNum) { + return [ + 'http://localhost:5610/test_bundle/built_css.css', + + `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.distFilename}`, + 'http://localhost:5610/built_assets/dlls/vendors.bundle.dll.js', + + shardNum === undefined + ? `http://localhost:5610/bundles/tests.bundle.js` + : `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, + + // this causes tilemap tests to fail, probably because the eui styles haven't been + // included in the karma harness a long some time, if ever + // `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, + 'http://localhost:5610/built_assets/dlls/vendors.style.dll.css', + 'http://localhost:5610/bundles/tests.style.css', + ]; + } + const config = { options: { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -90,15 +110,7 @@ module.exports = function(grunt) { }, // list of files / patterns to load in the browser - files: [ - 'http://localhost:5610/test_bundle/built_css.css', - - 'http://localhost:5610/built_assets/dlls/vendors.bundle.dll.js', - 'http://localhost:5610/bundles/tests.bundle.js', - - 'http://localhost:5610/built_assets/dlls/vendors.style.dll.css', - 'http://localhost:5610/bundles/tests.style.css', - ], + files: getKarmaFiles(), proxies: { '/tests/': 'http://localhost:5610/tests/', @@ -181,15 +193,7 @@ module.exports = function(grunt) { config[`ciShard-${n}`] = { singleRun: true, options: { - files: [ - 'http://localhost:5610/test_bundle/built_css.css', - - 'http://localhost:5610/built_assets/dlls/vendors.bundle.dll.js', - `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${n}`, - - 'http://localhost:5610/built_assets/dlls/vendors.style.dll.css', - 'http://localhost:5610/bundles/tests.style.css', - ], + files: getKarmaFiles(n), }, }; }); diff --git a/tasks/config/run.js b/tasks/config/run.js index a29061c9a7240..857895d75595c 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -152,6 +152,7 @@ module.exports = function(grunt) { args: [ 'nyc', '--reporter=html', + '--reporter=json-summary', '--report-dir=./target/kibana-coverage/mocha', NODE, 'scripts/mocha', diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 7854e2cd49837..7b7293dc9a037 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -29,6 +29,21 @@ const TEST_TAGS = safeLoad(JOBS_YAML) .JOB.filter(id => id.startsWith('kibana-ciGroup')) .map(id => id.replace(/^kibana-/, '')); +const getDefaultArgs = tag => { + return [ + 'scripts/functional_tests', + '--include-tag', + tag, + '--config', + 'test/functional/config.js', + '--config', + 'test/ui_capabilities/newsfeed_err/config.ts', + // '--config', 'test/functional/config.firefox.js', + '--bail', + '--debug', + ]; +}; + export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) { return { // include a run task for each test group @@ -38,18 +53,8 @@ export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) { [`functionalTests_${tag}`]: { cmd: process.execPath, args: [ - 'scripts/functional_tests', - '--include-tag', - tag, - '--config', - 'test/functional/config.js', - '--config', - 'test/ui_capabilities/newsfeed_err/config.ts', - // '--config', 'test/functional/config.firefox.js', - '--bail', - '--debug', - '--kibana-install-dir', - kibanaInstallDir, + ...getDefaultArgs(tag), + ...(!!process.env.CODE_COVERAGE ? [] : ['--kibana-install-dir', kibanaInstallDir]), ], }, }), diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index 5b02ba7e72430..5ddbd8649589c 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -25,15 +25,13 @@ export default function({ getService }) { const es = getService('legacyEs'); const createStatsMetric = eventName => ({ - key: ReportManager.createMetricKey({ appName: 'myApp', type: METRIC_TYPE.CLICK, eventName }), eventName, appName: 'myApp', type: METRIC_TYPE.CLICK, - stats: { sum: 1, avg: 1, min: 1, max: 1 }, + count: 1, }); const createUserAgentMetric = appName => ({ - key: ReportManager.createMetricKey({ appName, type: METRIC_TYPE.USER_AGENT }), appName, type: METRIC_TYPE.USER_AGENT, userAgent: @@ -42,12 +40,9 @@ export default function({ getService }) { describe('ui_metric API', () => { it('increments the count field in the document defined by the {app}/{action_type} path', async () => { + const reportManager = new ReportManager(); const uiStatsMetric = createStatsMetric('myEvent'); - const report = { - uiStatsMetrics: { - [uiStatsMetric.key]: uiStatsMetric, - }, - }; + const { report } = reportManager.assignReports([uiStatsMetric]); await supertest .post('/api/ui_metric/report') .set('kbn-xsrf', 'kibana') @@ -61,21 +56,18 @@ export default function({ getService }) { }); it('supports multiple events', async () => { + const reportManager = new ReportManager(); const userAgentMetric = createUserAgentMetric('kibana'); const uiStatsMetric1 = createStatsMetric('myEvent'); const hrTime = process.hrtime(); const nano = hrTime[0] * 1000000000 + hrTime[1]; const uniqueEventName = `myEvent${nano}`; const uiStatsMetric2 = createStatsMetric(uniqueEventName); - const report = { - userAgent: { - [userAgentMetric.key]: userAgentMetric, - }, - uiStatsMetrics: { - [uiStatsMetric1.key]: uiStatsMetric1, - [uiStatsMetric2.key]: uiStatsMetric2, - }, - }; + const { report } = reportManager.assignReports([ + userAgentMetric, + uiStatsMetric1, + uiStatsMetric2, + ]); await supertest .post('/api/ui_metric/report') .set('kbn-xsrf', 'kibana') diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts new file mode 100644 index 0000000000000..cc2fa23825498 --- /dev/null +++ b/test/common/services/security/role_mappings.ts @@ -0,0 +1,66 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class RoleMappings { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role_mappings' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(name: string, roleMapping: Record) { + this.log.debug(`creating role mapping ${name}`); + const { data, status, statusText } = await this.axios.post( + `/internal/security/role_mapping/${name}`, + roleMapping + ); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`created role mapping ${name}`); + } + + public async delete(name: string) { + this.log.debug(`deleting role mapping ${name}`); + const { data, status, statusText } = await this.axios.delete( + `/internal/security/role_mapping/${name}` + ); + if (status !== 200 && status !== 404) { + throw new Error( + `Expected status code of 200 or 404, received ${status} ${statusText}: ${util.inspect( + data + )}` + ); + } + this.log.debug(`deleted role mapping ${name}`); + } +} diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 6649a765a9e50..4eebb7b6697e0 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -21,6 +21,7 @@ import { format as formatUrl } from 'url'; import { Role } from './role'; import { User } from './user'; +import { RoleMappings } from './role_mappings'; import { FtrProviderContext } from '../../ftr_provider_context'; export function SecurityServiceProvider({ getService }: FtrProviderContext) { @@ -30,6 +31,7 @@ export function SecurityServiceProvider({ getService }: FtrProviderContext) { return new (class SecurityService { role = new Role(url, log); + roleMappings = new RoleMappings(url, log); user = new User(url, log); })(); } diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index 3caab3db44cb3..3b9e404e9b94d 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -27,7 +27,14 @@ import { } from '../../../../src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_constants'; export default function({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['dashboard', 'visualize', 'header', 'discover']); + const PageObjects = getPageObjects([ + 'dashboard', + 'visualize', + 'header', + 'discover', + 'tileMap', + 'visChart', + ]); const testSubjects = getService('testSubjects'); const browser = getService('browser'); const queryBar = getService('queryBar'); @@ -58,14 +65,14 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.switchToEditMode(); - await PageObjects.visualize.openLegendOptionColors('Count'); - await PageObjects.visualize.selectNewLegendColorChoice('#EA6460'); + await PageObjects.visChart.openLegendOptionColors('Count'); + await PageObjects.visChart.selectNewLegendColorChoice('#EA6460'); await PageObjects.dashboard.saveDashboard('Overridden colors'); await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.loadSavedDashboard('Overridden colors'); - const colorChoiceRetained = await PageObjects.visualize.doesSelectedLegendColorExist( + const colorChoiceRetained = await PageObjects.visChart.doesSelectedLegendColorExist( '#EA6460' ); @@ -153,10 +160,10 @@ export default function({ getService, getPageObjects }) { await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); - await PageObjects.visualize.clickMapZoomIn(); - await PageObjects.visualize.clickMapZoomIn(); - await PageObjects.visualize.clickMapZoomIn(); - await PageObjects.visualize.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); await PageObjects.visualize.saveVisualizationExpectSuccess('Visualization TileMap'); @@ -225,8 +232,8 @@ export default function({ getService, getPageObjects }) { describe('for embeddable config color parameters on a visualization', () => { it('updates a pie slice color on a soft refresh', async function() { await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME); - await PageObjects.visualize.openLegendOptionColors('80,000'); - await PageObjects.visualize.selectNewLegendColorChoice('#F9D9F9'); + await PageObjects.visChart.openLegendOptionColors('80,000'); + await PageObjects.visChart.selectNewLegendColorChoice('#F9D9F9'); const currentUrl = await browser.getCurrentUrl(); const newUrl = currentUrl.replace('F9D9F9', 'FFFFFF'); await browser.get(newUrl.toString(), false); @@ -248,7 +255,7 @@ export default function({ getService, getPageObjects }) { // Unskip once https://github.com/elastic/kibana/issues/15736 is fixed. it.skip('and updates the pie slice legend color', async function() { await retry.try(async () => { - const colorExists = await PageObjects.visualize.doesSelectedLegendColorExist('#FFFFFF'); + const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#FFFFFF'); expect(colorExists).to.be(true); }); }); @@ -269,7 +276,7 @@ export default function({ getService, getPageObjects }) { // Unskip once https://github.com/elastic/kibana/issues/15736 is fixed. it.skip('resets the legend color as well', async function() { await retry.try(async () => { - const colorExists = await PageObjects.visualize.doesSelectedLegendColorExist('#57c17b'); + const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#57c17b'); expect(colorExists).to.be(true); }); }); diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js new file mode 100644 index 0000000000000..4780f36fc27c6 --- /dev/null +++ b/test/functional/apps/discover/_discover_histogram.js @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +export default function({ getService, getPageObjects }) { + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); + const defaultSettings = { + defaultIndex: 'long-window-logstash-*', + 'dateFormat:tz': 'Europe/Berlin', + }; + + describe('discover histogram', function describeIndexTests() { + before(async function() { + log.debug('load kibana index with default index pattern'); + await PageObjects.common.navigateToApp('home'); + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('long_window_logstash'); + await esArchiver.load('visualize'); + await esArchiver.load('discover'); + + log.debug('create long_window_logstash index pattern'); + // NOTE: long_window_logstash load does NOT create index pattern + await PageObjects.settings.createIndexPattern('long-window-logstash-'); + await kibanaServer.uiSettings.replace(defaultSettings); + await browser.refresh(); + + log.debug('discover'); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('long-window-logstash-*'); + // NOTE: For some reason without setting this relative time, the abs times will not fetch data. + await PageObjects.timePicker.setCommonlyUsedTime('superDatePickerCommonlyUsed_Last_1 year'); + }); + after(async () => { + await esArchiver.unload('long_window_logstash'); + await esArchiver.unload('visualize'); + await esArchiver.unload('discover'); + }); + + it('should visualize monthly data with different day intervals', async () => { + //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 + const fromTime = '2017-11-01 00:00:00.000'; + const toTime = '2018-03-21 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.discover.setChartInterval('Monthly'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const chartCanvasExist = await PageObjects.discover.chartCanvasExist(); + expect(chartCanvasExist).to.be(true); + }); + it('should visualize weekly data with within DST changes', async () => { + //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 + const fromTime = '2018-03-01 00:00:00.000'; + const toTime = '2018-05-01 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.discover.setChartInterval('Weekly'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const chartCanvasExist = await PageObjects.discover.chartCanvasExist(); + expect(chartCanvasExist).to.be(true); + }); + it('should visualize monthly data with different years Scaled to 30d', async () => { + //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 + const fromTime = '2010-01-01 00:00:00.000'; + const toTime = '2018-03-21 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.discover.setChartInterval('Daily'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const chartCanvasExist = await PageObjects.discover.chartCanvasExist(); + expect(chartCanvasExist).to.be(true); + }); + }); +} diff --git a/test/functional/apps/discover/index.js b/test/functional/apps/discover/index.js index e10e772e93ab1..64a5a61335365 100644 --- a/test/functional/apps/discover/index.js +++ b/test/functional/apps/discover/index.js @@ -34,6 +34,7 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./_saved_queries')); loadTestFile(require.resolve('./_discover')); + loadTestFile(require.resolve('./_discover_histogram')); loadTestFile(require.resolve('./_filter_editor')); loadTestFile(require.resolve('./_errors')); loadTestFile(require.resolve('./_field_data')); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 04d81f4b46083..5af1676cf423f 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -23,7 +23,14 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); - const PageObjects = getPageObjects(['console', 'common', 'settings', 'visualize']); + const PageObjects = getPageObjects([ + 'console', + 'common', + 'settings', + 'visualize', + 'visEditor', + 'visChart', + ]); // https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html @@ -63,11 +70,11 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch('shakes*'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); const expectedChartValues = [111396]; await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData('Count'); + const data = await PageObjects.visChart.getBarChartData('Count'); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data[0] - expectedChartValues[0]).to.be.lessThan(5); @@ -84,22 +91,22 @@ export default function({ getService, getPageObjects }) { it('should configure metric Unique Count Speaking Parts', async function() { log.debug('Metric = Unique Count, speaker, Speaking Parts'); // this first change to the YAxis metric agg uses the default aggIndex of 1 - await PageObjects.visualize.selectYAxisAggregation( + await PageObjects.visEditor.selectYAxisAggregation( 'Unique Count', 'speaker', 'Speaking Parts' ); // then increment the aggIndex for the next one we create aggIndex = aggIndex + 1; - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const expectedChartValues = [935]; await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData('Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); }); - const title = await PageObjects.visualize.getYAxisTitle(); + const title = await PageObjects.visChart.getYAxisTitle(); expect(title).to.be('Speaking Parts'); }); @@ -110,23 +117,23 @@ export default function({ getService, getPageObjects }) { 5. Click Apply changes images/apply-changes-button.png to view the results. */ it('should configure Terms aggregation on play_name', async function() { - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); aggIndex = aggIndex + 1; log.debug('Field = play_name'); - await PageObjects.visualize.selectField('play_name'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('play_name'); + await PageObjects.visEditor.clickGo(); const expectedChartValues = [71, 65, 62, 55, 55]; await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData('Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); }); - const labels = await PageObjects.visualize.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(); expect(labels).to.eql([ 'Richard III', 'Henry VI Part 2', @@ -145,21 +152,21 @@ export default function({ getService, getPageObjects }) { 2. Choose the Max aggregation and select the speech_number field. */ it('should configure Max aggregation metric on speech_number', async function() { - await PageObjects.visualize.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); log.debug('Aggregation = Max'); - await PageObjects.visualize.selectYAxisAggregation( + await PageObjects.visEditor.selectYAxisAggregation( 'Max', 'speech_number', 'Max Speaking Parts', aggIndex ); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const expectedChartValues = [71, 65, 62, 55, 55]; const expectedChartValues2 = [177, 106, 153, 132, 162]; await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData('Speaking Parts'); - const data2 = await PageObjects.visualize.getBarChartData('Max Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); + const data2 = await PageObjects.visChart.getBarChartData('Max Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); log.debug('data2=' + data2); @@ -168,7 +175,7 @@ export default function({ getService, getPageObjects }) { expect(data2).to.eql(expectedChartValues2); }); - const labels = await PageObjects.visualize.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(); expect(labels).to.eql([ 'Richard III', 'Henry VI Part 2', @@ -184,15 +191,15 @@ export default function({ getService, getPageObjects }) { 4. Click Apply changes images/apply-changes-button.png. Your chart should now look like this: */ it('should configure change options to normal bars', async function() { - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.selectChartMode('normal'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.selectChartMode('normal'); + await PageObjects.visEditor.clickGo(); const expectedChartValues = [71, 65, 62, 55, 55]; const expectedChartValues2 = [177, 106, 153, 132, 162]; await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData('Speaking Parts'); - const data2 = await PageObjects.visualize.getBarChartData('Max Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); + const data2 = await PageObjects.visChart.getBarChartData('Max Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); log.debug('data2=' + data2); @@ -210,15 +217,15 @@ export default function({ getService, getPageObjects }) { Save this chart with the name Bar Example. */ it('should change the Y-Axis extents', async function() { - await PageObjects.visualize.setAxisExtents('50', '250'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setAxisExtents('50', '250'); + await PageObjects.visEditor.clickGo(); // same values as previous test except scaled down by the 50 for Y-Axis min const expectedChartValues = [21, 15, 12, 5, 5]; const expectedChartValues2 = [127, 56, 103, 82, 112]; await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData('Speaking Parts'); - const data2 = await PageObjects.visualize.getBarChartData('Max Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); + const data2 = await PageObjects.visChart.getBarChartData('Max Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); log.debug('data2=' + data2); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 06406bddeb009..3d9368f8d4680 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -74,7 +74,7 @@ export default function({ getService, getPageObjects }) { const toTime = 'Nov 19, 2016 @ 05:00:00.000'; await PageObjects.common.navigateToApp('discover'); - await PageObjects.discover.selectIndexPattern('alias2'); + await PageObjects.discover.selectIndexPattern('alias2*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await retry.try(async function() { diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index b8fa253f72104..e52cfdf478c33 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -24,7 +24,14 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const browser = getService('browser'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'visEditor', + 'visChart', + 'header', + 'timePicker', + ]); describe('area charts', function indexPatternCreation() { const vizName1 = 'Visualization AreaChart Name Test'; @@ -38,17 +45,17 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Click X-axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Click Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); log.debug('Check field value'); - const fieldValues = await PageObjects.visualize.getField(); + const fieldValues = await PageObjects.visEditor.getField(); log.debug('fieldValue = ' + fieldValues); expect(fieldValues[0]).to.be('@timestamp'); - const intervalValue = await PageObjects.visualize.getInterval(); + const intervalValue = await PageObjects.visEditor.getInterval(); log.debug('intervalValue = ' + intervalValue); expect(intervalValue[0]).to.be('Auto'); - return PageObjects.visualize.clickGo(); + return PageObjects.visEditor.clickGo(); }; before(initAreaChart); @@ -70,7 +77,7 @@ export default function({ getService, getPageObjects }) { it('should save and load', async function() { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -113,14 +120,14 @@ export default function({ getService, getPageObjects }) { ]; await retry.try(async function tryingForTime() { - const labels = await PageObjects.visualize.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(); log.debug('X-Axis labels = ' + labels); expect(labels).to.eql(xAxisLabels); }); - const labels = await PageObjects.visualize.getYAxisLabels(); + const labels = await PageObjects.visChart.getYAxisLabels(); log.debug('Y-Axis labels = ' + labels); expect(labels).to.eql(yAxisLabels); - const paths = await PageObjects.visualize.getAreaChartData('Count'); + const paths = await PageObjects.visChart.getAreaChartData('Count'); log.debug('expectedAreaChartData = ' + expectedAreaChartData); log.debug('actual chart data = ' + paths); expect(paths).to.eql(expectedAreaChartData); @@ -185,9 +192,9 @@ export default function({ getService, getPageObjects }) { ['2015-09-20 19:00', '55'], ]; - await PageObjects.visualize.toggleOpenEditor(2); - await PageObjects.visualize.setInterval('Second'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(2); + await PageObjects.visEditor.setInterval('Second'); + await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableData(expectedTableData); await inspector.close(); @@ -217,9 +224,9 @@ export default function({ getService, getPageObjects }) { ['2015-09-20 19:00', '0.015'], ]; - await PageObjects.visualize.toggleAdvancedParams('2'); - await PageObjects.visualize.toggleScaleMetrics(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleAdvancedParams('2'); + await PageObjects.visEditor.toggleScaleMetrics(); + await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableData(expectedTableData); await inspector.close(); @@ -249,11 +256,11 @@ export default function({ getService, getPageObjects }) { ['2015-09-20 19:00', '55', '2.053KB'], ]; - await PageObjects.visualize.clickBucket('Y-axis', 'metrics'); - await PageObjects.visualize.selectAggregation('Top Hit', 'metrics'); - await PageObjects.visualize.selectField('bytes', 'metrics'); - await PageObjects.visualize.selectAggregateWith('average'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.selectAggregation('Top Hit', 'metrics'); + await PageObjects.visEditor.selectField('bytes', 'metrics'); + await PageObjects.visEditor.selectAggregateWith('average'); + await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableData(expectedTableData); await inspector.close(); @@ -285,13 +292,13 @@ export default function({ getService, getPageObjects }) { const axisId = 'ValueAxis-1'; it('should show ticks on selecting log scale', async () => { - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions(axisId); - await PageObjects.visualize.selectYAxisScaleType(axisId, 'log'); - await PageObjects.visualize.clickYAxisAdvancedOptions(axisId); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); + await PageObjects.visEditor.clickYAxisAdvancedOptions(axisId); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -317,9 +324,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting log scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -345,10 +352,10 @@ export default function({ getService, getPageObjects }) { }); it('should show ticks on selecting square root scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'square root'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '0', '200', @@ -364,18 +371,18 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting square root scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'linear'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); const expectedLabels = [ '0', @@ -392,9 +399,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting linear scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); @@ -412,16 +419,16 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch('long-window-logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); log.debug('Click X-axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Click Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setInterval('Yearly'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Yearly'); + await PageObjects.visEditor.clickGo(); // This svg area is composed by 7 years (2013 - 2019). // 7 points are used to draw the upper line (usually called y1) // 7 points compose the lower line (usually called y0) - const paths = await PageObjects.visualize.getAreaChartPaths('Count'); + const paths = await PageObjects.visChart.getAreaChartPaths('Count'); log.debug('actual chart data = ' + paths); const numberOfSegments = 7 * 2; expect(paths.length).to.eql(numberOfSegments); @@ -435,17 +442,17 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch('long-window-logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); log.debug('Click X-axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Click Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setInterval('Monthly'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Monthly'); + await PageObjects.visEditor.clickGo(); // This svg area is composed by 67 months 3 (2013) + 5 * 12 + 4 (2019) // 67 points are used to draw the upper line (usually called y1) // 67 points compose the lower line (usually called y0) const numberOfSegments = 67 * 2; - const paths = await PageObjects.visualize.getAreaChartPaths('Count'); + const paths = await PageObjects.visChart.getAreaChartPaths('Count'); log.debug('actual chart data = ' + paths); expect(paths.length).to.eql(numberOfSegments); }); diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js index e8fe8fb656877..0a9ff1e77a2ef 100644 --- a/test/functional/apps/visualize/_data_table.js +++ b/test/functional/apps/visualize/_data_table.js @@ -22,9 +22,10 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const log = getService('log'); const inspector = getService('inspector'); + const testSubjects = getService('testSubjects'); const retry = getService('retry'); const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'timePicker', 'visEditor', 'visChart']); describe('data table', function indexPatternCreation() { const vizName1 = 'Visualization DataTable'; @@ -38,27 +39,27 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Split rows'); - await PageObjects.visualize.clickBucket('Split rows'); + await PageObjects.visEditor.clickBucket('Split rows'); log.debug('Aggregation = Histogram'); - await PageObjects.visualize.selectAggregation('Histogram'); + await PageObjects.visEditor.selectAggregation('Histogram'); log.debug('Field = bytes'); - await PageObjects.visualize.selectField('bytes'); + await PageObjects.visEditor.selectField('bytes'); log.debug('Interval = 2000'); - await PageObjects.visualize.setNumericInterval('2000'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); + await PageObjects.visEditor.clickGo(); }); it('should allow applying changed params', async () => { - await PageObjects.visualize.setNumericInterval('1', { append: true }); - const interval = await PageObjects.visualize.getNumericInterval(); + await PageObjects.visEditor.setInterval('1', { type: 'numeric', append: true }); + const interval = await PageObjects.visEditor.getNumericInterval(); expect(interval).to.be('20001'); - const isApplyButtonEnabled = await PageObjects.visualize.isApplyEnabled(); + const isApplyButtonEnabled = await PageObjects.visEditor.isApplyEnabled(); expect(isApplyButtonEnabled).to.be(true); }); it('should allow reseting changed params', async () => { - await PageObjects.visualize.clickReset(); - const interval = await PageObjects.visualize.getNumericInterval(); + await PageObjects.visEditor.clickReset(); + const interval = await PageObjects.visEditor.getNumericInterval(); expect(interval).to.be('2000'); }); @@ -66,7 +67,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -96,7 +97,7 @@ export default function({ getService, getPageObjects }) { it('should show percentage columns', async () => { async function expectValidTableData() { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql([ '≥ 0 and < 1000', '1,351 64.7%', @@ -110,16 +111,16 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Range'); - await PageObjects.visualize.selectField('bytes'); - await PageObjects.visualize.clickGo(); - await PageObjects.visualize.clickOptionsTab(); - await PageObjects.visualize.setSelectByOptionText( + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Range'); + await PageObjects.visEditor.selectField('bytes'); + await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickOptionsTab(); + await PageObjects.visEditor.setSelectByOptionText( 'datatableVisualizationPercentageCol', 'Count' ); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); await expectValidTableData(); @@ -128,20 +129,20 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(SAVE_NAME); await PageObjects.visualize.loadSavedVisualization(SAVE_NAME); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); await expectValidTableData(); // check that it works after selecting a column that's deleted - await PageObjects.visualize.clickData(); - await PageObjects.visualize.clickBucket('Metric', 'metrics'); - await PageObjects.visualize.selectAggregation('Average', 'metrics'); - await PageObjects.visualize.selectField('bytes', 'metrics'); - await PageObjects.visualize.removeDimension(1); - await PageObjects.visualize.clickGo(); - await PageObjects.visualize.clickOptionsTab(); - - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickDataTab(); + await PageObjects.visEditor.clickBucket('Metric', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average', 'metrics'); + await PageObjects.visEditor.selectField('bytes', 'metrics'); + await PageObjects.visEditor.removeDimension(1); + await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickOptionsTab(); + + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql([ '≥ 0 and < 1000', '344.094B', @@ -155,12 +156,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Metric', 'metrics'); - await PageObjects.visualize.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visualize.selectAggregation('Terms', 'metrics', 'buckets'); - await PageObjects.visualize.selectField('geo.src', 'metrics', 'buckets'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Metric', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); + await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets'); + await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); }); @@ -170,12 +171,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setInterval('Daily'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql([ '2015-09-20', @@ -192,12 +193,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setInterval('Daily'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql([ '2015-09-20', '4,757', @@ -210,15 +211,15 @@ export default function({ getService, getPageObjects }) { it('should correctly filter for applied time filter on the main timefield', async () => { await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); }); it('should correctly filter for pinned filters', async () => { await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); }); @@ -227,11 +228,11 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickMetricEditor(); - await PageObjects.visualize.selectAggregation('Top Hit', 'metrics'); - await PageObjects.visualize.selectField('agent.raw', 'metrics'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickMetricEditor(); + await PageObjects.visEditor.selectAggregation('Top Hit', 'metrics'); + await PageObjects.visEditor.selectField('agent.raw', 'metrics'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data); expect(data.length).to.be.greaterThan(0); }); @@ -241,11 +242,11 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Range'); - await PageObjects.visualize.selectField('bytes'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Range'); + await PageObjects.visEditor.selectField('bytes'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql([ '≥ 0 and < 1000', '1,351', @@ -260,19 +261,19 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('extension.raw'); - await PageObjects.visualize.setSize(2); - await PageObjects.visualize.clickGo(); - - await PageObjects.visualize.toggleOtherBucket(); - await PageObjects.visualize.toggleMissingBucket(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('extension.raw'); + await PageObjects.visEditor.setSize(2); + await PageObjects.visEditor.clickGo(); + + await PageObjects.visEditor.toggleOtherBucket(); + await PageObjects.visEditor.toggleMissingBucket(); + await PageObjects.visEditor.clickGo(); }); it('should show correct data', async () => { - const data = await PageObjects.visualize.getTableVisContent(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ ['jpg', '9,109'], ['css', '2,159'], @@ -281,9 +282,9 @@ export default function({ getService, getPageObjects }) { }); it('should apply correct filter', async () => { - await PageObjects.visualize.filterOnTableCell(1, 3); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visualize.getTableVisContent(); + await PageObjects.visChart.filterOnTableCell(1, 3); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ ['png', '1,373'], ['gif', '918'], @@ -298,20 +299,20 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('extension.raw'); - await PageObjects.visualize.setSize(2); - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('geo.dest'); - await PageObjects.visualize.toggleOpenEditor(3, 'false'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('extension.raw'); + await PageObjects.visEditor.setSize(2); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('geo.dest'); + await PageObjects.visEditor.toggleOpenEditor(3, 'false'); + await PageObjects.visEditor.clickGo(); }); it('should show correct data without showMetricsAtAllLevels', async () => { - const data = await PageObjects.visualize.getTableVisContent(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ ['jpg', 'CN', '1,718'], ['jpg', 'IN', '1,511'], @@ -327,10 +328,10 @@ export default function({ getService, getPageObjects }) { }); it('should show correct data without showMetricsAtAllLevels even if showPartialRows is selected', async () => { - await PageObjects.visualize.clickOptionsTab(); - await PageObjects.visualize.checkCheckbox('showPartialRows'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisContent(); + await PageObjects.visEditor.clickOptionsTab(); + await testSubjects.setCheckbox('showPartialRows', 'check'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ ['jpg', 'CN', '1,718'], ['jpg', 'IN', '1,511'], @@ -346,10 +347,10 @@ export default function({ getService, getPageObjects }) { }); it('should show metrics on each level', async () => { - await PageObjects.visualize.clickOptionsTab(); - await PageObjects.visualize.checkCheckbox('showMetricsAtAllLevels'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisContent(); + await PageObjects.visEditor.clickOptionsTab(); + await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ ['jpg', '9,109', 'CN', '1,718'], ['jpg', '9,109', 'IN', '1,511'], @@ -365,12 +366,12 @@ export default function({ getService, getPageObjects }) { }); it('should show metrics other than count on each level', async () => { - await PageObjects.visualize.clickData(); - await PageObjects.visualize.clickBucket('Metric', 'metrics'); - await PageObjects.visualize.selectAggregation('Average', 'metrics'); - await PageObjects.visualize.selectField('bytes', 'metrics'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisContent(); + await PageObjects.visEditor.clickDataTab(); + await PageObjects.visEditor.clickBucket('Metric', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average', 'metrics'); + await PageObjects.visEditor.selectField('bytes', 'metrics'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ ['jpg', '9,109', '5.469KB', 'CN', '1,718', '5.477KB'], ['jpg', '9,109', '5.469KB', 'IN', '1,511', '5.456KB'], @@ -392,26 +393,26 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split table'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('extension.raw'); - await PageObjects.visualize.setSize(2); - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('geo.dest'); - await PageObjects.visualize.setSize(3, 3); - await PageObjects.visualize.toggleOpenEditor(3, 'false'); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('geo.src'); - await PageObjects.visualize.setSize(3, 4); - await PageObjects.visualize.toggleOpenEditor(4, 'false'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split table'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('extension.raw'); + await PageObjects.visEditor.setSize(2); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('geo.dest'); + await PageObjects.visEditor.setSize(3, 3); + await PageObjects.visEditor.toggleOpenEditor(3, 'false'); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('geo.src'); + await PageObjects.visEditor.setSize(3, 4); + await PageObjects.visEditor.toggleOpenEditor(4, 'false'); + await PageObjects.visEditor.clickGo(); }); it('should have a splitted table', async () => { - const data = await PageObjects.visualize.getTableVisContent(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ [ ['CN', 'CN', '330'], @@ -439,10 +440,10 @@ export default function({ getService, getPageObjects }) { }); it('should show metrics for split bucket when using showMetricsAtAllLevels', async () => { - await PageObjects.visualize.clickOptionsTab(); - await PageObjects.visualize.checkCheckbox('showMetricsAtAllLevels'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisContent(); + await PageObjects.visEditor.clickOptionsTab(); + await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ [ ['CN', '1,718', 'CN', '330'], diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js index 77478f5d10edc..3db3cd094a81b 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.js +++ b/test/functional/apps/visualize/_data_table_nontimeindex.js @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const filterBar = getService('filterBar'); const renderable = getService('renderable'); - const PageObjects = getPageObjects(['common', 'visualize', 'header']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'header', 'visChart']); describe.skip('data table with index without time filter', function indexPatternCreation() { const vizName1 = 'Visualization DataTable without time filter'; @@ -40,27 +40,27 @@ export default function({ getService, getPageObjects }) { PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED ); log.debug('Bucket = Split Rows'); - await PageObjects.visualize.clickBucket('Split rows'); + await PageObjects.visEditor.clickBucket('Split rows'); log.debug('Aggregation = Histogram'); - await PageObjects.visualize.selectAggregation('Histogram'); + await PageObjects.visEditor.selectAggregation('Histogram'); log.debug('Field = bytes'); - await PageObjects.visualize.selectField('bytes'); + await PageObjects.visEditor.selectField('bytes'); log.debug('Interval = 2000'); - await PageObjects.visualize.setNumericInterval('2000'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); + await PageObjects.visEditor.clickGo(); }); it('should allow applying changed params', async () => { - await PageObjects.visualize.setNumericInterval('1', { append: true }); - const interval = await PageObjects.visualize.getNumericInterval(); + await PageObjects.visEditor.setInterval('1', { type: 'numeric', append: true }); + const interval = await PageObjects.visEditor.getNumericInterval(); expect(interval).to.be('20001'); - const isApplyButtonEnabled = await PageObjects.visualize.isApplyEnabled(); + const isApplyButtonEnabled = await PageObjects.visEditor.isApplyEnabled(); expect(isApplyButtonEnabled).to.be(true); }); it('should allow reseting changed params', async () => { - await PageObjects.visualize.clickReset(); - const interval = await PageObjects.visualize.getNumericInterval(); + await PageObjects.visEditor.clickReset(); + const interval = await PageObjects.visEditor.getNumericInterval(); expect(interval).to.be('2000'); }); @@ -68,7 +68,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -102,12 +102,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch( PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED ); - await PageObjects.visualize.clickBucket('Metric', 'metrics'); - await PageObjects.visualize.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visualize.selectAggregation('Terms', 'metrics', 'buckets'); - await PageObjects.visualize.selectField('geo.src', 'metrics', 'buckets'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Metric', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); + await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets'); + await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); }); @@ -118,12 +118,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch( PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED ); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setInterval('Daily'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql([ '2015-09-20', @@ -141,12 +141,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch( PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED ); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setInterval('Daily'); - await PageObjects.visualize.clickGo(); - const data = await PageObjects.visualize.getTableVisData(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql([ '2015-09-20', '4,757', @@ -161,7 +161,7 @@ export default function({ getService, getPageObjects }) { await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); }); @@ -169,7 +169,7 @@ export default function({ getService, getPageObjects }) { await filterBar.toggleFilterPinned('@timestamp'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); }); }); diff --git a/test/functional/apps/visualize/_embedding_chart.js b/test/functional/apps/visualize/_embedding_chart.js index c16dc3b1279ff..940aa3eb5d462 100644 --- a/test/functional/apps/visualize/_embedding_chart.js +++ b/test/functional/apps/visualize/_embedding_chart.js @@ -24,7 +24,13 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const renderable = getService('renderable'); const embedding = getService('embedding'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'visualize', + 'visEditor', + 'visChart', + 'header', + 'timePicker', + ]); describe('embedding', () => { describe('a data table', () => { @@ -33,22 +39,22 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split rows'); - await PageObjects.visualize.selectAggregation('Histogram'); - await PageObjects.visualize.selectField('bytes'); - await PageObjects.visualize.setNumericInterval('2000', undefined, 3); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Histogram'); + await PageObjects.visEditor.selectField('bytes'); + await PageObjects.visEditor.setInterval('2000', { type: 'numeric', aggNth: 3 }); + await PageObjects.visEditor.clickGo(); }); it('should allow opening table vis in embedded mode', async () => { await embedding.openInEmbeddedMode(); await renderable.waitForRender(); - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql([ '2015-09-20 00:00', @@ -89,7 +95,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql([ '2015-09-21 00:00', @@ -126,11 +132,11 @@ export default function({ getService, getPageObjects }) { }); it('should allow to change timerange from the visualization in embedded mode', async () => { - await PageObjects.visualize.filterOnTableCell(1, 7); + await PageObjects.visChart.filterOnTableCell(1, 7); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); log.debug(data.split('\n')); expect(data.trim().split('\n')).to.be.eql([ '03:00', diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index ae364244b4f5c..2ce15cf913eff 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default ({ getService, getPageObjects }) => { const log = getService('log'); - const PageObjects = getPageObjects(['common', 'visualize']); + const PageObjects = getPageObjects(['visualize']); describe('visualize app', function() { this.tags('smoke'); diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 2b9033d1ae9d2..7ebb4548f967b 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -24,7 +24,7 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const inspector = getService('inspector'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); // FLAKY: https://github.com/elastic/kibana/issues/45089 describe('gauge chart', function indexPatternCreation() { @@ -50,24 +50,24 @@ export default function({ getService, getPageObjects }) { // initial metric of "Count" is selected by default return retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getGaugeValue(); + const metricValue = await PageObjects.visChart.getGaugeValue(); expect(expectedCount).to.eql(metricValue); }); }); it('should show Split Gauges', async function() { log.debug('Bucket = Split Group'); - await PageObjects.visualize.clickBucket('Split group'); + await PageObjects.visEditor.clickBucket('Split group'); log.debug('Aggregation = Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Field = machine.os.raw'); - await PageObjects.visualize.selectField('machine.os.raw'); + await PageObjects.visEditor.selectField('machine.os.raw'); log.debug('Size = 4'); - await PageObjects.visualize.setSize('4'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setSize('4'); + await PageObjects.visEditor.clickGo(); await retry.try(async () => { - expect(await PageObjects.visualize.getGaugeValue()).to.eql([ + expect(await PageObjects.visChart.getGaugeValue()).to.eql([ '2,904', 'win 8', '2,858', @@ -83,34 +83,34 @@ export default function({ getService, getPageObjects }) { it('should show correct values for fields with fieldFormatters', async function() { const expectedTexts = ['2,904', 'win 8: Count', '0B', 'win 8: Min bytes']; - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('machine.os.raw'); - await PageObjects.visualize.setSize('1'); - await PageObjects.visualize.clickBucket('Metric', 'metrics'); - await PageObjects.visualize.selectAggregation('Min', 'metrics'); - await PageObjects.visualize.selectField('bytes', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os.raw'); + await PageObjects.visEditor.setSize('1'); + await PageObjects.visEditor.clickBucket('Metric', 'metrics'); + await PageObjects.visEditor.selectAggregation('Min', 'metrics'); + await PageObjects.visEditor.selectField('bytes', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getGaugeValue(); + const metricValue = await PageObjects.visChart.getGaugeValue(); expect(expectedTexts).to.eql(metricValue); }); }); it('should format the metric correctly in percentage mode', async function() { await initGaugeVis(); - await PageObjects.visualize.clickMetricEditor(); - await PageObjects.visualize.selectAggregation('Average', 'metrics'); - await PageObjects.visualize.selectField('bytes', 'metrics'); - await PageObjects.visualize.clickOptionsTab(); + await PageObjects.visEditor.clickMetricEditor(); + await PageObjects.visEditor.selectAggregation('Average', 'metrics'); + await PageObjects.visEditor.selectField('bytes', 'metrics'); + await PageObjects.visEditor.clickOptionsTab(); await testSubjects.setValue('gaugeColorRange2__to', '10000'); await testSubjects.click('gaugePercentageMode'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - await PageObjects.visualize.clickGo(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { const expectedTexts = ['57.273%', 'Average bytes']; - const metricValue = await PageObjects.visualize.getGaugeValue(); + const metricValue = await PageObjects.visChart.getGaugeValue(); expect(expectedTexts).to.eql(metricValue); }); }); diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index 226e490a6b232..2cea861d0f64d 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const log = getService('log'); const inspector = getService('inspector'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('heatmap chart', function indexPatternCreation() { this.tags('smoke'); @@ -36,20 +36,20 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = X-Axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); log.debug('Field = @timestamp'); - await PageObjects.visualize.selectField('@timestamp'); + await PageObjects.visEditor.selectField('@timestamp'); // leaving Interval set to Auto - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); }); it('should save and load', async function() { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -87,16 +87,16 @@ export default function({ getService, getPageObjects }) { }); it('should show 4 color ranges as default colorNumbers param', async function() { - const legends = await PageObjects.visualize.getLegendEntries(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = ['0 - 400', '400 - 800', '800 - 1,200', '1,200 - 1,600']; expect(legends).to.eql(expectedLegends); }); it('should show 6 color ranges if changed on options', async function() { - await PageObjects.visualize.clickOptionsTab(); - await PageObjects.visualize.changeHeatmapColorNumbers(6); - await PageObjects.visualize.clickGo(); - const legends = await PageObjects.visualize.getLegendEntries(); + await PageObjects.visEditor.clickOptionsTab(); + await PageObjects.visEditor.changeHeatmapColorNumbers(6); + await PageObjects.visEditor.clickGo(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 267', '267 - 534', @@ -108,23 +108,23 @@ export default function({ getService, getPageObjects }) { expect(legends).to.eql(expectedLegends); }); it('should show 6 custom color ranges', async function() { - await PageObjects.visualize.clickOptionsTab(); - await PageObjects.visualize.clickEnableCustomRanges(); - await PageObjects.visualize.clickAddRange(); - await PageObjects.visualize.clickAddRange(); - await PageObjects.visualize.clickAddRange(); - await PageObjects.visualize.clickAddRange(); - await PageObjects.visualize.clickAddRange(); - await PageObjects.visualize.clickAddRange(); - await PageObjects.visualize.clickAddRange(); + await PageObjects.visEditor.clickOptionsTab(); + await PageObjects.visEditor.clickEnableCustomRanges(); + await PageObjects.visEditor.clickAddRange(); + await PageObjects.visEditor.clickAddRange(); + await PageObjects.visEditor.clickAddRange(); + await PageObjects.visEditor.clickAddRange(); + await PageObjects.visEditor.clickAddRange(); + await PageObjects.visEditor.clickAddRange(); + await PageObjects.visEditor.clickAddRange(); log.debug('customize 2 last ranges'); - await PageObjects.visualize.setCustomRangeByIndex(6, '650', '720'); - await PageObjects.visualize.setCustomRangeByIndex(7, '800', '905'); + await PageObjects.visEditor.setCustomRangeByIndex(6, '650', '720'); + await PageObjects.visEditor.setCustomRangeByIndex(7, '800', '905'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - await PageObjects.visualize.clickGo(); - const legends = await PageObjects.visualize.getLegendEntries(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 100', '100 - 200', diff --git a/test/functional/apps/visualize/_histogram_request_start.js b/test/functional/apps/visualize/_histogram_request_start.js index a601e370a568f..a74fa8856a3b3 100644 --- a/test/functional/apps/visualize/_histogram_request_start.js +++ b/test/functional/apps/visualize/_histogram_request_start.js @@ -22,7 +22,13 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'visEditor', + 'visChart', + 'timePicker', + ]); describe('histogram agg onSearchRequestStart', function() { before(async function() { @@ -33,21 +39,21 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Split Rows'); - await PageObjects.visualize.clickBucket('Split rows'); + await PageObjects.visEditor.clickBucket('Split rows'); log.debug('Aggregation = Histogram'); - await PageObjects.visualize.selectAggregation('Histogram'); + await PageObjects.visEditor.selectAggregation('Histogram'); log.debug('Field = machine.ram'); - await PageObjects.visualize.selectField('machine.ram'); + await PageObjects.visEditor.selectField('machine.ram'); }); describe('interval parameter uses autoBounds', function() { it('should use provided value when number of generated buckets is less than histogram:maxBars', async function() { const providedInterval = 2400000000; log.debug(`Interval = ${providedInterval}`); - await PageObjects.visualize.setNumericInterval(providedInterval); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); + await PageObjects.visEditor.clickGo(); await retry.try(async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); const dataArray = data.replace(/,/g, '').split('\n'); expect(dataArray.length).to.eql(20); const bucketStart = parseInt(dataArray[0], 10); @@ -60,11 +66,11 @@ export default function({ getService, getPageObjects }) { it('should scale value to round number when number of generated buckets is greater than histogram:maxBars', async function() { const providedInterval = 100; log.debug(`Interval = ${providedInterval}`); - await PageObjects.visualize.setNumericInterval(providedInterval); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); + await PageObjects.visEditor.clickGo(); await PageObjects.common.sleep(1000); //fix this await retry.try(async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); const dataArray = data.replace(/,/g, '').split('\n'); expect(dataArray.length).to.eql(20); const bucketStart = parseInt(dataArray[0], 10); diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index 76b75f62b5f2a..84f955d9c7879 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -21,7 +21,7 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { this.tags('smoke'); @@ -39,10 +39,10 @@ export default function({ getService, getPageObjects }) { await inspector.expectTableHeaders(['Count']); log.debug('Add Average Metric on machine.ram field'); - await PageObjects.visualize.clickBucket('Y-axis', 'metrics'); - await PageObjects.visualize.selectAggregation('Average', 'metrics'); - await PageObjects.visualize.selectField('machine.ram', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average', 'metrics'); + await PageObjects.visEditor.selectField('machine.ram', 'metrics'); + await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableHeaders(['Count', 'Average machine.ram']); }); @@ -50,23 +50,23 @@ export default function({ getService, getPageObjects }) { describe('filtering on inspector table values', function() { before(async function() { log.debug('Add X-axis terms agg on machine.os.raw'); - await PageObjects.visualize.clickBucket('X-axis'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('machine.os.raw'); - await PageObjects.visualize.setSize(2); - await PageObjects.visualize.toggleOtherBucket(3); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('X-axis'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os.raw'); + await PageObjects.visEditor.setSize(2); + await PageObjects.visEditor.toggleOtherBucket(3); + await PageObjects.visEditor.clickGo(); }); beforeEach(async function() { await inspector.open(); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); }); afterEach(async function() { await inspector.close(); await filterBar.removeFilter('machine.os.raw'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); }); it('should allow filtering for values', async function() { diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 6a0a21e76924d..becf66f0fd5b1 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -24,7 +24,13 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'visEditor', + 'visChart', + 'timePicker', + ]); describe('line charts', function() { const vizName1 = 'Visualization LineChart'; @@ -37,14 +43,14 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Split chart'); - await PageObjects.visualize.clickBucket('Split chart'); + await PageObjects.visEditor.clickBucket('Split chart'); log.debug('Aggregation = Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Field = extension'); - await PageObjects.visualize.selectField('extension.raw'); + await PageObjects.visEditor.selectField('extension.raw'); log.debug('switch from Rows to Columns'); - await PageObjects.visualize.clickSplitDirection('Columns'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickSplitDirection('Columns'); + await PageObjects.visEditor.clickGo(); }; before(initLineChart); @@ -60,7 +66,7 @@ export default function({ getService, getPageObjects }) { // sleep a bit before trying to get the chart data await PageObjects.common.sleep(3000); - const data = await PageObjects.visualize.getLineChartData(); + const data = await PageObjects.visChart.getLineChartData(); log.debug('data=' + data); const tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (let x = 0; x < data.length; x++) { @@ -91,10 +97,10 @@ export default function({ getService, getPageObjects }) { const expectedChartData = ['png 1,373', 'php 445', 'jpg 9,109', 'gif 918', 'css 2,159']; log.debug('Order By = Term'); - await PageObjects.visualize.selectOrderByMetric(2, '_key'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectOrderByMetric(2, '_key'); + await PageObjects.visEditor.clickGo(); await retry.try(async function() { - const data = await PageObjects.visualize.getLineChartData(); + const data = await PageObjects.visChart.getLineChartData(); log.debug('data=' + data); const tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (let x = 0; x < data.length; x++) { @@ -172,7 +178,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); describe.skip('switch between Y axis scale types', () => { @@ -180,13 +186,13 @@ export default function({ getService, getPageObjects }) { const axisId = 'ValueAxis-1'; it('should show ticks on selecting log scale', async () => { - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions(axisId); - await PageObjects.visualize.selectYAxisScaleType(axisId, 'log'); - await PageObjects.visualize.clickYAxisAdvancedOptions(axisId); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); + await PageObjects.visEditor.clickYAxisAdvancedOptions(axisId); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -212,9 +218,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting log scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -240,36 +246,36 @@ export default function({ getService, getPageObjects }) { }); it('should show ticks on selecting square root scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'square root'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting square root scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'linear'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting linear scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; expect(labels).to.eql(expectedLabels); }); diff --git a/test/functional/apps/visualize/_linked_saved_searches.js b/test/functional/apps/visualize/_linked_saved_searches.js index 9bbe8c9d2147c..37ec3f06f2ecd 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.js +++ b/test/functional/apps/visualize/_linked_saved_searches.js @@ -22,7 +22,14 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discover', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'visualize', + 'header', + 'timePicker', + 'visChart', + ]); describe('visualize app', function describeIndexTests() { describe('linked saved searched', () => { @@ -43,7 +50,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickSavedSearch(savedSearchName); await PageObjects.timePicker.setDefaultAbsoluteRange(); await retry.waitFor('wait for count to equal 9,109', async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); return data.trim() === '9,109'; }); }); @@ -54,7 +61,7 @@ export default function({ getService, getPageObjects }) { 'Sep 21, 2015 @ 10:00:00.000' ); await retry.waitFor('wait for count to equal 3,950', async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); return data.trim() === '3,950'; }); }); @@ -63,7 +70,7 @@ export default function({ getService, getPageObjects }) { await filterBar.addFilter('bytes', 'is between', '100', '3000'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 707', async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); return data.trim() === '707'; }); }); @@ -71,7 +78,7 @@ export default function({ getService, getPageObjects }) { it('should allow unlinking from a linked search', async () => { await PageObjects.visualize.clickUnlinkSavedSearch(); await retry.waitFor('wait for count to equal 707', async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); return data.trim() === '707'; }); // The filter on the saved search should now be in the editor @@ -82,7 +89,7 @@ export default function({ getService, getPageObjects }) { await filterBar.toggleFilterEnabled('extension.raw'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 1,293', async () => { - const unfilteredData = await PageObjects.visualize.getTableVisData(); + const unfilteredData = await PageObjects.visChart.getTableVisData(); return unfilteredData.trim() === '1,293'; }); }); @@ -91,7 +98,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccess('Unlinked before saved'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 1,293', async () => { - const data = await PageObjects.visualize.getTableVisData(); + const data = await PageObjects.visChart.getTableVisData(); return data.trim() === '1,293'; }); }); diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js index 870c465f2de37..51c03c90f507b 100644 --- a/test/functional/apps/visualize/_markdown_vis.js +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; export default function({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'visualize', 'header']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'header']); const find = getService('find'); const inspector = getService('inspector'); const markdown = ` @@ -33,8 +33,8 @@ export default function({ getPageObjects, getService }) { before(async function() { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); - await PageObjects.visualize.setMarkdownTxt(markdown); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setMarkdownTxt(markdown); + await PageObjects.visEditor.clickGo(); }); describe('markdown vis', () => { @@ -43,29 +43,29 @@ export default function({ getPageObjects, getService }) { }); it('should render markdown as html', async function() { - const h1Txt = await PageObjects.visualize.getMarkdownBodyDescendentText('h1'); + const h1Txt = await PageObjects.visChart.getMarkdownBodyDescendentText('h1'); expect(h1Txt).to.equal('Heading 1'); }); it('should not render html in markdown as html', async function() { const expected = 'Heading 1\n

Inline HTML that should not be rendered as html

'; - const actual = await PageObjects.visualize.getMarkdownText(); + const actual = await PageObjects.visChart.getMarkdownText(); expect(actual).to.equal(expected); }); it('should auto apply changes if auto mode is turned on', async function() { const markdown2 = '## Heading 2'; - await PageObjects.visualize.toggleAutoMode(); - await PageObjects.visualize.setMarkdownTxt(markdown2); + await PageObjects.visEditor.toggleAutoMode(); + await PageObjects.visEditor.setMarkdownTxt(markdown2); await PageObjects.header.waitUntilLoadingHasFinished(); - const h1Txt = await PageObjects.visualize.getMarkdownBodyDescendentText('h2'); + const h1Txt = await PageObjects.visChart.getMarkdownBodyDescendentText('h2'); expect(h1Txt).to.equal('Heading 2'); }); it('should resize the editor', async function() { const editorSidebar = await find.byCssSelector('.visEditor__sidebar'); const initialSize = await editorSidebar.getSize(); - await PageObjects.visualize.sizeUpEditor(); + await PageObjects.visEditor.sizeUpEditor(); const afterSize = await editorSidebar.getSize(); expect(afterSize.width).to.be.greaterThan(initialSize.width); }); diff --git a/test/functional/apps/visualize/_metric_chart.js b/test/functional/apps/visualize/_metric_chart.js index 31140a1718cfe..6a95f7553943c 100644 --- a/test/functional/apps/visualize/_metric_chart.js +++ b/test/functional/apps/visualize/_metric_chart.js @@ -24,7 +24,7 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const filterBar = getService('filterBar'); const inspector = getService('inspector'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('metric chart', function() { before(async function() { @@ -45,21 +45,21 @@ export default function({ getService, getPageObjects }) { // initial metric of "Count" is selected by default await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(expectedCount).to.eql(metricValue); }); }); it('should show Average', async function() { const avgMachineRam = ['13,104,036,080.615', 'Average machine.ram']; - await PageObjects.visualize.clickMetricEditor(); + await PageObjects.visEditor.clickMetricEditor(); log.debug('Aggregation = Average'); - await PageObjects.visualize.selectAggregation('Average', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average', 'metrics'); log.debug('Field = machine.ram'); - await PageObjects.visualize.selectField('machine.ram', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('machine.ram', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(avgMachineRam).to.eql(metricValue); }); }); @@ -67,12 +67,12 @@ export default function({ getService, getPageObjects }) { it('should show Sum', async function() { const sumPhpMemory = ['85,865,880', 'Sum of phpmemory']; log.debug('Aggregation = Sum'); - await PageObjects.visualize.selectAggregation('Sum', 'metrics'); + await PageObjects.visEditor.selectAggregation('Sum', 'metrics'); log.debug('Field = phpmemory'); - await PageObjects.visualize.selectField('phpmemory', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('phpmemory', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(sumPhpMemory).to.eql(metricValue); }); }); @@ -81,12 +81,12 @@ export default function({ getService, getPageObjects }) { const medianBytes = ['5,565.263', '50th percentile of bytes']; // For now, only comparing the text label part of the metric log.debug('Aggregation = Median'); - await PageObjects.visualize.selectAggregation('Median', 'metrics'); + await PageObjects.visEditor.selectAggregation('Median', 'metrics'); log.debug('Field = bytes'); - await PageObjects.visualize.selectField('bytes', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('bytes', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); // only comparing the text label! expect(medianBytes[1]).to.eql(metricValue[1]); }); @@ -95,12 +95,12 @@ export default function({ getService, getPageObjects }) { it('should show Min', async function() { const minTimestamp = ['Sep 20, 2015 @ 00:00:00.000', 'Min @timestamp']; log.debug('Aggregation = Min'); - await PageObjects.visualize.selectAggregation('Min', 'metrics'); + await PageObjects.visEditor.selectAggregation('Min', 'metrics'); log.debug('Field = @timestamp'); - await PageObjects.visualize.selectField('@timestamp', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('@timestamp', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(minTimestamp).to.eql(metricValue); }); }); @@ -111,12 +111,12 @@ export default function({ getService, getPageObjects }) { 'Max relatedContent.article:modified_time', ]; log.debug('Aggregation = Max'); - await PageObjects.visualize.selectAggregation('Max', 'metrics'); + await PageObjects.visEditor.selectAggregation('Max', 'metrics'); log.debug('Field = relatedContent.article:modified_time'); - await PageObjects.visualize.selectField('relatedContent.article:modified_time', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('relatedContent.article:modified_time', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(maxRelatedContentArticleModifiedTime).to.eql(metricValue); }); }); @@ -124,12 +124,12 @@ export default function({ getService, getPageObjects }) { it('should show Unique Count', async function() { const uniqueCountClientip = ['1,000', 'Unique count of clientip']; log.debug('Aggregation = Unique Count'); - await PageObjects.visualize.selectAggregation('Unique Count', 'metrics'); + await PageObjects.visEditor.selectAggregation('Unique Count', 'metrics'); log.debug('Field = clientip'); - await PageObjects.visualize.selectField('clientip', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('clientip', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(uniqueCountClientip).to.eql(metricValue); }); }); @@ -153,12 +153,12 @@ export default function({ getService, getPageObjects }) { ]; log.debug('Aggregation = Percentiles'); - await PageObjects.visualize.selectAggregation('Percentiles', 'metrics'); + await PageObjects.visEditor.selectAggregation('Percentiles', 'metrics'); log.debug('Field = machine.ram'); - await PageObjects.visualize.selectField('machine.ram', 'metrics'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('machine.ram', 'metrics'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(percentileMachineRam).to.eql(metricValue); }); }); @@ -166,14 +166,14 @@ export default function({ getService, getPageObjects }) { it('should show Percentile Ranks', async function() { const percentileRankBytes = ['2.036%', 'Percentile rank 99 of "memory"']; log.debug('Aggregation = Percentile Ranks'); - await PageObjects.visualize.selectAggregation('Percentile Ranks', 'metrics'); + await PageObjects.visEditor.selectAggregation('Percentile Ranks', 'metrics'); log.debug('Field = bytes'); - await PageObjects.visualize.selectField('memory', 'metrics'); + await PageObjects.visEditor.selectField('memory', 'metrics'); log.debug('Values = 99'); - await PageObjects.visualize.setValue('99'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setValue('99'); + await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.visualize.getMetric(); + const metricValue = await PageObjects.visChart.getMetric(); expect(percentileRankBytes).to.eql(metricValue); }); }); @@ -183,7 +183,7 @@ export default function({ getService, getPageObjects }) { let filterCount = 0; await retry.try(async function tryingForTime() { // click first metric bucket - await PageObjects.visualize.clickMetricByIndex(0); + await PageObjects.visEditor.clickMetricByIndex(0); filterCount = await filterBar.getFilterCount(); }); expect(filterCount).to.equal(0); @@ -191,17 +191,17 @@ export default function({ getService, getPageObjects }) { it('should allow filtering with buckets', async function() { log.debug('Bucket = Split Group'); - await PageObjects.visualize.clickBucket('Split group'); + await PageObjects.visEditor.clickBucket('Split group'); log.debug('Aggregation = Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Field = machine.os.raw'); - await PageObjects.visualize.selectField('machine.os.raw'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('machine.os.raw'); + await PageObjects.visEditor.clickGo(); let filterCount = 0; await retry.try(async function tryingForTime() { // click first metric bucket - await PageObjects.visualize.clickMetricByIndex(0); + await PageObjects.visEditor.clickMetricByIndex(0); filterCount = await filterBar.getFilterCount(); }); await filterBar.removeAllFilters(); diff --git a/test/functional/apps/visualize/_pie_chart.js b/test/functional/apps/visualize/_pie_chart.js index 03067bb2182c5..313a4e39e5030 100644 --- a/test/functional/apps/visualize/_pie_chart.js +++ b/test/functional/apps/visualize/_pie_chart.js @@ -24,7 +24,14 @@ export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const pieChart = getService('pieChart'); const inspector = getService('inspector'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'visEditor', + 'visChart', + 'header', + 'timePicker', + ]); describe('pie chart', function() { const vizName1 = 'Visualization PieChart'; @@ -36,24 +43,24 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); + await PageObjects.visEditor.clickBucket('Split slices'); log.debug('Click aggregation Histogram'); - await PageObjects.visualize.selectAggregation('Histogram'); + await PageObjects.visEditor.selectAggregation('Histogram'); log.debug('Click field memory'); - await PageObjects.visualize.selectField('memory'); + await PageObjects.visEditor.selectField('memory'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); log.debug('setNumericInterval 4000'); - await PageObjects.visualize.setNumericInterval('40000'); + await PageObjects.visEditor.setInterval('40000', { type: 'numeric' }); log.debug('clickGo'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); }); it('should save and load', async function() { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -93,15 +100,15 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); + await PageObjects.visEditor.clickBucket('Split slices'); log.debug('Click aggregation Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Click field machine.os.raw'); - await PageObjects.visualize.selectField('machine.os.raw'); - await PageObjects.visualize.toggleOtherBucket(2); - await PageObjects.visualize.toggleMissingBucket(2); + await PageObjects.visEditor.selectField('machine.os.raw'); + await PageObjects.visEditor.toggleOtherBucket(2); + await PageObjects.visEditor.toggleMissingBucket(2); log.debug('clickGo'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); await pieChart.expectPieChartLabels(expectedTableData); }); @@ -109,20 +116,20 @@ export default function({ getService, getPageObjects }) { const expectedTableData = ['Missing', 'osx']; await pieChart.filterOnPieSlice('Other'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); await pieChart.expectPieChartLabels(expectedTableData); await filterBar.removeFilter('machine.os.raw'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should apply correct filter on other bucket by clicking on a legend', async () => { const expectedTableData = ['Missing', 'osx']; - await PageObjects.visualize.filterLegend('Other'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.filterLegend('Other'); + await PageObjects.visChart.waitForVisualization(); await pieChart.expectPieChartLabels(expectedTableData); await filterBar.removeFilter('machine.os.raw'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should show two levels of other buckets', async () => { @@ -171,15 +178,15 @@ export default function({ getService, getPageObjects }) { 'Other', ]; - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split slices'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split slices'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Click field geo.src'); - await PageObjects.visualize.selectField('geo.src'); - await PageObjects.visualize.toggleOtherBucket(3); - await PageObjects.visualize.toggleMissingBucket(3); + await PageObjects.visEditor.selectField('geo.src'); + await PageObjects.visEditor.toggleOtherBucket(3); + await PageObjects.visEditor.toggleMissingBucket(3); log.debug('clickGo'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); await pieChart.expectPieChartLabels(expectedTableData); }); }); @@ -187,17 +194,17 @@ export default function({ getService, getPageObjects }) { describe('disabled aggs', () => { before(async () => { await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForRenderingCount(); + await PageObjects.visChart.waitForRenderingCount(); }); it('should show correct result with one agg disabled', async () => { const expectedTableData = ['win 8', 'win xp', 'win 7', 'ios', 'osx']; - await PageObjects.visualize.clickBucket('Split slices'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('machine.os.raw'); - await PageObjects.visualize.toggleDisabledAgg(2); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split slices'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os.raw'); + await PageObjects.visEditor.toggleDisabledAgg(2); + await PageObjects.visEditor.clickGo(); await pieChart.expectPieChartLabels(expectedTableData); }); @@ -206,15 +213,15 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForRenderingCount(); + await PageObjects.visChart.waitForRenderingCount(); const expectedTableData = ['win 8', 'win xp', 'win 7', 'ios', 'osx']; await pieChart.expectPieChartLabels(expectedTableData); }); it('should show correct result when agg is re-enabled', async () => { - await PageObjects.visualize.toggleDisabledAgg(2); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleDisabledAgg(2); + await PageObjects.visEditor.clickGo(); const expectedTableData = [ '0', @@ -291,24 +298,24 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); + await PageObjects.visEditor.clickBucket('Split slices'); log.debug('Click aggregation Filters'); - await PageObjects.visualize.selectAggregation('Filters'); + await PageObjects.visEditor.selectAggregation('Filters'); log.debug('Set the 1st filter value'); - await PageObjects.visualize.setFilterAggregationValue('geo.dest:"US"'); + await PageObjects.visEditor.setFilterAggregationValue('geo.dest:"US"'); log.debug('Add new filter'); - await PageObjects.visualize.addNewFilterAggregation(); + await PageObjects.visEditor.addNewFilterAggregation(); log.debug('Set the 2nd filter value'); - await PageObjects.visualize.setFilterAggregationValue('geo.dest:"CN"', 1); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setFilterAggregationValue('geo.dest:"CN"', 1); + await PageObjects.visEditor.clickGo(); const emptyFromTime = 'Sep 19, 2016 @ 06:31:44.000'; const emptyToTime = 'Sep 23, 2016 @ 18:31:44.000'; log.debug( 'Switch to a different time range from "' + emptyFromTime + '" to "' + emptyToTime + '"' ); await PageObjects.timePicker.setAbsoluteRange(emptyFromTime, emptyToTime); - await PageObjects.visualize.waitForVisualization(); - await PageObjects.visualize.expectError(); + await PageObjects.visChart.waitForVisualization(); + await PageObjects.visChart.expectError(); }); }); describe('multi series slice', () => { @@ -327,22 +334,22 @@ export default function({ getService, getPageObjects }) { ); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); + await PageObjects.visEditor.clickBucket('Split slices'); log.debug('Click aggregation Histogram'); - await PageObjects.visualize.selectAggregation('Histogram'); + await PageObjects.visEditor.selectAggregation('Histogram'); log.debug('Click field memory'); - await PageObjects.visualize.selectField('memory'); + await PageObjects.visEditor.selectField('memory'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); log.debug('setNumericInterval 4000'); - await PageObjects.visualize.setNumericInterval('40000'); + await PageObjects.visEditor.setInterval('40000', { type: 'numeric' }); log.debug('Toggle previous editor'); - await PageObjects.visualize.toggleAggregationEditor(2); + await PageObjects.visEditor.toggleAggregationEditor(2); log.debug('select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('geo.dest'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split slices'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('geo.dest'); + await PageObjects.visEditor.clickGo(); }); it('should show correct chart', async () => { @@ -428,11 +435,11 @@ export default function({ getService, getPageObjects }) { '360,000', 'CN', ]; - await PageObjects.visualize.filterLegend('CN'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.filterLegend('CN'); + await PageObjects.visChart.waitForVisualization(); await pieChart.expectPieChartLabels(expectedTableData); await filterBar.removeFilter('geo.dest'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should still showing pie chart when a subseries have zero data', async function() { @@ -442,21 +449,21 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); + await PageObjects.visEditor.clickBucket('Split slices'); log.debug('Click aggregation Filters'); - await PageObjects.visualize.selectAggregation('Filters'); + await PageObjects.visEditor.selectAggregation('Filters'); log.debug('Set the 1st filter value'); - await PageObjects.visualize.setFilterAggregationValue('geo.dest:"US"'); + await PageObjects.visEditor.setFilterAggregationValue('geo.dest:"US"'); log.debug('Toggle previous editor'); - await PageObjects.visualize.toggleAggregationEditor(2); + await PageObjects.visEditor.toggleAggregationEditor(2); log.debug('Add a new series, select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); + await PageObjects.visEditor.clickBucket('Split slices'); log.debug('Click aggregation Filters'); - await PageObjects.visualize.selectAggregation('Filters'); + await PageObjects.visEditor.selectAggregation('Filters'); log.debug('Set the 1st filter value of the aggregation id 3'); - await PageObjects.visualize.setFilterAggregationValue('geo.dest:"UX"', 0, 3); - await PageObjects.visualize.clickGo(); - const legends = await PageObjects.visualize.getLegendEntries(); + await PageObjects.visEditor.setFilterAggregationValue('geo.dest:"UX"', 0, 3); + await PageObjects.visEditor.clickGo(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = ['geo.dest:"US"', 'geo.dest:"UX"']; expect(legends).to.eql(expectedLegends); }); @@ -469,15 +476,15 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split chart'); - await PageObjects.visualize.clickBucket('Split chart'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('machine.os.raw'); - await PageObjects.visualize.toggleAggregationEditor(2); + await PageObjects.visEditor.clickBucket('Split chart'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os.raw'); + await PageObjects.visEditor.toggleAggregationEditor(2); log.debug('Add a new series, select bucket Split slices'); - await PageObjects.visualize.clickBucket('Split slices'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('geo.src'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('Split slices'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('geo.src'); + await PageObjects.visEditor.clickGo(); }); it('shows correct split chart', async () => { @@ -522,7 +529,7 @@ export default function({ getService, getPageObjects }) { ['ios', '478', 'CN', '478'], ['osx', '228', 'CN', '228'], ]; - await PageObjects.visualize.filterLegend('CN'); + await PageObjects.visChart.filterLegend('CN'); await PageObjects.header.waitUntilLoadingHasFinished(); await inspector.open(); await inspector.setTablePageSize(50); diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index 2d496cb575db7..e7ce5808554b4 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -25,11 +25,12 @@ export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const PageObjects = getPageObjects([ - 'common', 'visualize', 'header', 'pointSeries', 'timePicker', + 'visEditor', + 'visChart', ]); const pointSeriesVis = PageObjects.pointSeries; const inspector = getService('inspector'); @@ -42,18 +43,18 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = X-axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); log.debug('Field = @timestamp'); - await PageObjects.visualize.selectField('@timestamp'); + await PageObjects.visEditor.selectField('@timestamp'); // add another metrics log.debug('Metric = Value Axis'); - await PageObjects.visualize.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); log.debug('Aggregation = Average'); - await PageObjects.visualize.selectAggregation('Average', 'metrics'); + await PageObjects.visEditor.selectAggregation('Average', 'metrics'); log.debug('Field = memory'); - await PageObjects.visualize.selectField('machine.ram', 'metrics'); + await PageObjects.visEditor.selectField('machine.ram', 'metrics'); // go to options page log.debug('Going to axis options'); await pointSeriesVis.clickAxisOptions(); @@ -61,11 +62,11 @@ export default function({ getService, getPageObjects }) { log.debug('adding axis'); await pointSeriesVis.clickAddAxis(); // set average count to use second value axis - await PageObjects.visualize.toggleAccordion('visEditorSeriesAccordion3'); + await PageObjects.visEditor.toggleAccordion('visEditorSeriesAccordion3'); log.debug('Average memory value axis - ValueAxis-2'); await pointSeriesVis.setSeriesAxis(1, 'ValueAxis-2'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - await PageObjects.visualize.clickGo(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); } describe('point series', function describeIndexTests() { @@ -129,14 +130,14 @@ export default function({ getService, getPageObjects }) { ]; await retry.try(async () => { - const data = await PageObjects.visualize.getLineChartData('Count'); + const data = await PageObjects.visChart.getLineChartData('Count'); log.debug('count data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues[0]); }); await retry.try(async () => { - const avgMemoryData = await PageObjects.visualize.getLineChartData( + const avgMemoryData = await PageObjects.visChart.getLineChartData( 'Average machine.ram', 'ValueAxis-2' ); @@ -158,7 +159,7 @@ export default function({ getService, getPageObjects }) { describe('multiple chart types', function() { it('should change average series type to histogram', async function() { await pointSeriesVis.setSeriesType(1, 'histogram'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const length = await pointSeriesVis.getHistogramSeries(); expect(length).to.be(1); }); @@ -166,12 +167,12 @@ export default function({ getService, getPageObjects }) { describe('grid lines', function() { before(async function() { - await pointSeriesVis.clickOptions(); + await PageObjects.visEditor.clickOptionsTab(); }); it('should show category grid lines', async function() { await pointSeriesVis.toggleGridCategoryLines(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const gridLines = await pointSeriesVis.getGridLines(); expect(gridLines.length).to.be(9); gridLines.forEach(gridLine => { @@ -182,7 +183,7 @@ export default function({ getService, getPageObjects }) { it('should show value axis grid lines', async function() { await pointSeriesVis.setGridValueAxis('ValueAxis-2'); await pointSeriesVis.toggleGridCategoryLines(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const gridLines = await pointSeriesVis.getGridLines(); expect(gridLines.length).to.be(9); gridLines.forEach(gridLine => { @@ -199,36 +200,36 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickLineChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.visualize.selectYAxisAggregation('Average', 'bytes', customLabel, 1); - await PageObjects.visualize.clickGo(); - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions('ValueAxis-1'); + await PageObjects.visEditor.selectYAxisAggregation('Average', 'bytes', customLabel, 1); + await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions('ValueAxis-1'); }); it('should render a custom label when one is set', async function() { - const title = await PageObjects.visualize.getYAxisTitle(); + const title = await PageObjects.visChart.getYAxisTitle(); expect(title).to.be(customLabel); }); it('should render a custom axis title when one is set, overriding the custom label', async function() { await pointSeriesVis.setAxisTitle(axisTitle); - await PageObjects.visualize.clickGo(); - const title = await PageObjects.visualize.getYAxisTitle(); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); expect(title).to.be(axisTitle); }); it('should preserve saved axis titles after a vis is saved and reopened', async function() { await PageObjects.visualize.saveVisualizationExpectSuccess(visName); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); await PageObjects.visualize.loadSavedVisualization(visName); - await PageObjects.visualize.waitForRenderingCount(); - await PageObjects.visualize.clickData(); - await PageObjects.visualize.toggleOpenEditor(1); - await PageObjects.visualize.setCustomLabel('test', 1); - await PageObjects.visualize.clickGo(); - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions('ValueAxis-1'); - const title = await PageObjects.visualize.getYAxisTitle(); + await PageObjects.visChart.waitForRenderingCount(); + await PageObjects.visEditor.clickDataTab(); + await PageObjects.visEditor.toggleOpenEditor(1); + await PageObjects.visEditor.setCustomLabel('test', 1); + await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions('ValueAxis-1'); + const title = await PageObjects.visChart.getYAxisTitle(); expect(title).to.be(axisTitle); }); }); @@ -238,7 +239,7 @@ export default function({ getService, getPageObjects }) { it('should show round labels in default timezone', async function() { await initChart(); - const labels = await PageObjects.visualize.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(); expect(labels.join()).to.contain(expectedLabels.join()); }); @@ -248,7 +249,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.header.awaitKibanaChrome(); await initChart(); - const labels = await PageObjects.visualize.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(); expect(labels.join()).to.contain(expectedLabels.join()); }); @@ -258,10 +259,10 @@ export default function({ getService, getPageObjects }) { const toTime = 'Sep 22, 2015 @ 16:08:34.554'; // note that we're setting the absolute time range while we're in 'America/Phoenix' tz await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.visualize.waitForRenderingCount(); + await PageObjects.visChart.waitForRenderingCount(); await retry.waitFor('wait for x-axis labels to match expected for Phoenix', async () => { - const labels = await PageObjects.visualize.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(); log.debug(`Labels: ${labels}`); return ( labels.toString() === ['10:00', '11:00', '12:00', '13:00', '14:00', '15:00'].toString() @@ -303,11 +304,11 @@ export default function({ getService, getPageObjects }) { await browser.refresh(); // wait some time before trying to check for rendering count await PageObjects.header.awaitKibanaChrome(); - await PageObjects.visualize.waitForRenderingCount(); + await PageObjects.visChart.waitForRenderingCount(); log.debug('getXAxisLabels'); await retry.waitFor('wait for x-axis labels to match expected for UTC', async () => { - const labels2 = await PageObjects.visualize.getXAxisLabels(); + const labels2 = await PageObjects.visChart.getXAxisLabels(); log.debug(`Labels: ${labels2}`); return ( labels2.toString() === ['17:00', '18:00', '19:00', '20:00', '21:00', '22:00'].toString() diff --git a/test/functional/apps/visualize/_region_map.js b/test/functional/apps/visualize/_region_map.js index f6c81dab5921f..10cbd9913c70c 100644 --- a/test/functional/apps/visualize/_region_map.js +++ b/test/functional/apps/visualize/_region_map.js @@ -24,7 +24,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const log = getService('log'); const find = getService('find'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'settings']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'timePicker']); before(async function() { log.debug('navigateToApp visualize'); @@ -34,12 +34,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Shape field'); - await PageObjects.visualize.clickBucket('Shape field'); + await PageObjects.visEditor.clickBucket('Shape field'); log.debug('Aggregation = Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Field = geo.src'); - await PageObjects.visualize.selectField('geo.src'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectField('geo.src'); + await PageObjects.visEditor.clickGo(); }); describe('vector map', function indexPatternCreation() { @@ -60,26 +60,26 @@ export default function({ getService, getPageObjects }) { }); it('should change results after changing layer to world', async function() { - await PageObjects.visualize.clickOptions(); - await PageObjects.visualize.setSelectByOptionText( + await PageObjects.visEditor.clickOptionsTab(); + await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectLayer', 'World Countries' ); //ensure all fields are there - await PageObjects.visualize.setSelectByOptionText( + await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', 'ISO 3166-1 alpha-2 code' ); - await PageObjects.visualize.setSelectByOptionText( + await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', 'ISO 3166-1 alpha-3 code' ); - await PageObjects.visualize.setSelectByOptionText( + await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', 'name' ); - await PageObjects.visualize.setSelectByOptionText( + await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', 'ISO 3166-1 alpha-2 code' ); diff --git a/test/functional/apps/visualize/_tag_cloud.js b/test/functional/apps/visualize/_tag_cloud.js index 97b8036b30503..4f921cec1fdf1 100644 --- a/test/functional/apps/visualize/_tag_cloud.js +++ b/test/functional/apps/visualize/_tag_cloud.js @@ -26,7 +26,16 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const retry = getService('retry'); const find = getService('find'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'visEditor', + 'visChart', + 'header', + 'settings', + 'timePicker', + 'tagCloud', + ]); describe('tag cloud chart', function() { const vizName1 = 'Visualization tagCloud'; @@ -40,15 +49,15 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select Tags'); - await PageObjects.visualize.clickBucket('Tags'); + await PageObjects.visEditor.clickBucket('Tags'); log.debug('Click aggregation Terms'); - await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Click field machine.ram'); await retry.try(async function tryingForTime() { - await PageObjects.visualize.selectField(termsField); + await PageObjects.visEditor.selectField(termsField); }); - await PageObjects.visualize.selectOrderByMetric(2, '_key'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.selectOrderByMetric(2, '_key'); + await PageObjects.visEditor.clickGo(); }); it('should have inspector enabled', async function() { @@ -56,7 +65,7 @@ export default function({ getService, getPageObjects }) { }); it('should show correct tag cloud data', async function() { - const data = await PageObjects.visualize.getTextTag(); + const data = await PageObjects.tagCloud.getTextTag(); log.debug(data); expect(data).to.eql([ '32,212,254,720', @@ -69,22 +78,22 @@ export default function({ getService, getPageObjects }) { it('should collapse the sidebar', async function() { const editorSidebar = await find.byCssSelector('.collapsible-sidebar'); - await PageObjects.visualize.clickEditorSidebarCollapse(); + await PageObjects.visEditor.clickEditorSidebarCollapse(); // Give d3 tag cloud some time to rearrange tags await PageObjects.common.sleep(1000); const afterSize = await editorSidebar.getSize(); expect(afterSize.width).to.be(0); - await PageObjects.visualize.clickEditorSidebarCollapse(); + await PageObjects.visEditor.clickEditorSidebarCollapse(); }); it('should still show all tags after sidebar has been collapsed', async function() { - await PageObjects.visualize.clickEditorSidebarCollapse(); + await PageObjects.visEditor.clickEditorSidebarCollapse(); // Give d3 tag cloud some time to rearrange tags await PageObjects.common.sleep(1000); - await PageObjects.visualize.clickEditorSidebarCollapse(); + await PageObjects.visEditor.clickEditorSidebarCollapse(); // Give d3 tag cloud some time to rearrange tags await PageObjects.common.sleep(1000); - const data = await PageObjects.visualize.getTextTag(); + const data = await PageObjects.tagCloud.getTextTag(); log.debug(data); expect(data).to.eql([ '32,212,254,720', @@ -100,7 +109,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.sleep(1000); await browser.setWindowSize(1200, 800); await PageObjects.common.sleep(1000); - const data = await PageObjects.visualize.getTextTag(); + const data = await PageObjects.tagCloud.getTextTag(); expect(data).to.eql([ '32,212,254,720', '21,474,836,480', @@ -114,11 +123,11 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should show the tags and relative size', function() { - return PageObjects.visualize.getTextSizes().then(function(results) { + return PageObjects.tagCloud.getTextSizes().then(function(results) { log.debug('results here ' + results); expect(results).to.eql(['72px', '63px', '25px', '32px', '18px']); }); @@ -153,7 +162,7 @@ export default function({ getService, getPageObjects }) { }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); after(async function() { @@ -168,15 +177,15 @@ export default function({ getService, getPageObjects }) { }); it('should format tags with field formatter', async function() { - const data = await PageObjects.visualize.getTextTag(); + const data = await PageObjects.tagCloud.getTextTag(); log.debug(data); expect(data).to.eql(['30GB', '20GB', '19GB', '18GB', '17GB']); }); it('should apply filter with unformatted value', async function() { - await PageObjects.visualize.selectTagCloudTag('30GB'); + await PageObjects.tagCloud.selectTagCloudTag('30GB'); await PageObjects.header.waitUntilLoadingHasFinished(); - const data = await PageObjects.visualize.getTextTag(); + const data = await PageObjects.tagCloud.getTextTag(); expect(data).to.eql(['30GB']); }); }); diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index e2946339b1f08..397eaeb0f3013 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -27,7 +27,14 @@ export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'settings']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'visEditor', + 'visChart', + 'timePicker', + 'tileMap', + ]); describe('tile map visualize app', function() { describe('incomplete config', function describeIndexTests() { @@ -45,8 +52,8 @@ export default function({ getService, getPageObjects }) { it('should be able to zoom in twice', async () => { //should not throw - await PageObjects.visualize.clickMapZoomIn(); - await PageObjects.visualize.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); }); }); @@ -61,14 +68,14 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Geo Coordinates'); - await PageObjects.visualize.clickBucket('Geo coordinates'); + await PageObjects.visEditor.clickBucket('Geo coordinates'); log.debug('Click aggregation Geohash'); - await PageObjects.visualize.selectAggregation('Geohash'); + await PageObjects.visEditor.selectAggregation('Geohash'); log.debug('Click field geo.coordinates'); await retry.try(async function tryingForTime() { - await PageObjects.visualize.selectField('geo.coordinates'); + await PageObjects.visEditor.selectField('geo.coordinates'); }); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); }); /** @@ -106,9 +113,9 @@ export default function({ getService, getPageObjects }) { ['-', '8', '108', { lat: 18, lon: -157 }], ]; //level 1 - await PageObjects.visualize.clickMapZoomOut(); + await PageObjects.tileMap.clickMapZoomOut(); //level 0 - await PageObjects.visualize.clickMapZoomOut(); + await PageObjects.tileMap.clickMapZoomOut(); await inspector.open(); await inspector.setTablePageSize(50); @@ -118,8 +125,8 @@ export default function({ getService, getPageObjects }) { }); it('should not be able to zoom out beyond 0', async function() { - await PageObjects.visualize.zoomAllTheWayOut(); - const enabled = await PageObjects.visualize.getMapZoomOutEnabled(); + await PageObjects.tileMap.zoomAllTheWayOut(); + const enabled = await PageObjects.tileMap.getMapZoomOutEnabled(); expect(enabled).to.be(false); }); @@ -148,7 +155,7 @@ export default function({ getService, getPageObjects }) { ['-', 'b7', '167', { lat: 64, lon: -163 }], ]; - await PageObjects.visualize.clickMapFitDataBounds(); + await PageObjects.tileMap.clickMapFitDataBounds(); await inspector.open(); const data = await inspector.getTableData(); await inspector.close(); @@ -164,8 +171,8 @@ export default function({ getService, getPageObjects }) { await filterBar.addFilter('bytes', 'is between', '19980', '19990'); await filterBar.toggleFilterPinned('bytes'); - await PageObjects.visualize.zoomAllTheWayOut(); - await PageObjects.visualize.clickMapFitDataBounds(); + await PageObjects.tileMap.zoomAllTheWayOut(); + await PageObjects.tileMap.clickMapFitDataBounds(); await inspector.open(); const data = await inspector.getTableData(); @@ -178,15 +185,15 @@ export default function({ getService, getPageObjects }) { it('Newly saved visualization retains map bounds', async () => { const vizName1 = 'Visualization TileMap'; - await PageObjects.visualize.clickMapZoomIn(); - await PageObjects.visualize.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); - const mapBounds = await PageObjects.visualize.getMapBounds(); + const mapBounds = await PageObjects.tileMap.getMapBounds(); await inspector.close(); await PageObjects.visualize.saveVisualizationExpectSuccess(vizName1); - const afterSaveMapBounds = await PageObjects.visualize.getMapBounds(); + const afterSaveMapBounds = await PageObjects.tileMap.getMapBounds(); await inspector.close(); // For some reason the values are slightly different, so we can't check that they are equal. But we did @@ -207,17 +214,17 @@ export default function({ getService, getPageObjects }) { }); it('when not checked does not add filters to aggregation', async () => { - await PageObjects.visualize.toggleOpenEditor(2); - await PageObjects.visualize.setIsFilteredByCollarCheckbox(false); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(2); + await PageObjects.visEditor.setIsFilteredByCollarCheckbox(false); + await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableHeaders(['geohash_grid', 'Count', 'Geo Centroid']); await inspector.close(); }); after(async () => { - await PageObjects.visualize.setIsFilteredByCollarCheckbox(true); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setIsFilteredByCollarCheckbox(true); + await PageObjects.visEditor.clickGo(); }); }); }); @@ -245,18 +252,18 @@ export default function({ getService, getPageObjects }) { const zoomLevel = 9; for (let i = 0; i < zoomLevel; i++) { - await PageObjects.visualize.clickMapZoomIn(); + await PageObjects.tileMap.clickMapZoomIn(); } }); beforeEach(async function() { - await PageObjects.visualize.clickMapZoomIn(waitForLoading); + await PageObjects.tileMap.clickMapZoomIn(waitForLoading); }); afterEach(async function() { if (!last) { await PageObjects.common.sleep(toastDefaultLife); - await PageObjects.visualize.clickMapZoomOut(waitForLoading); + await PageObjects.tileMap.clickMapZoomOut(waitForLoading); } }); @@ -270,11 +277,11 @@ export default function({ getService, getPageObjects }) { it('should suppress zoom warning if suppress warnings button clicked', async () => { last = true; - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); await find.clickByCssSelector('[data-test-subj="suppressZoomWarnings"]'); - await PageObjects.visualize.clickMapZoomOut(waitForLoading); + await PageObjects.tileMap.clickMapZoomOut(waitForLoading); await testSubjects.waitForDeleted('suppressZoomWarnings'); - await PageObjects.visualize.clickMapZoomIn(waitForLoading); + await PageObjects.tileMap.clickMapZoomIn(waitForLoading); await testSubjects.missingOrFail('maxZoomWarning'); }); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index dc4b9a786eaa4..8dbe356889568 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); - const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); describe('visual builder', function describeIndexTests() { this.tags('smoke'); @@ -80,7 +80,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should verify gauge label and count display', async () => { - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const labelString = await PageObjects.visualBuilder.getGaugeLabel(); expect(labelString).to.be('Count'); const gaugeCount = await PageObjects.visualBuilder.getGaugeCount(); @@ -96,7 +96,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should verify topN label and count display', async () => { - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const labelString = await PageObjects.visualBuilder.getTopNLabel(); expect(labelString).to.be('Count'); const gaugeCount = await PageObjects.visualBuilder.getTopNCount(); diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index a36b6facb3039..5808212559b18 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -21,7 +21,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getPageObjects }: FtrProviderContext) { - const { visualBuilder, visualize } = getPageObjects(['visualBuilder', 'visualize']); + const { visualBuilder, visualize, visChart } = getPageObjects([ + 'visualBuilder', + 'visualize', + 'visChart', + ]); describe('visual builder', function describeIndexTests() { describe('table', () => { @@ -32,7 +36,7 @@ export default function({ getPageObjects }: FtrProviderContext) { await visualBuilder.checkTableTabIsPresent(); await visualBuilder.selectGroupByField('machine.os.raw'); await visualBuilder.setColumnLabelValue('OS'); - await visualize.waitForVisualizationRenderingStabilized(); + await visChart.waitForVisualizationRenderingStabilized(); }); it('should display correct values on changing group by field and column name', async () => { diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index 224dec7ef2a71..df0603c7f95f5 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['common', 'header', 'timePicker', 'visualize']); + const PageObjects = getPageObjects(['timePicker', 'visualize', 'visChart', 'vegaChart']); const filterBar = getService('filterBar'); const inspector = getService('inspector'); const log = getService('log'); @@ -40,7 +40,7 @@ export default function({ getService, getPageObjects }) { }); it.skip('should have some initial vega spec text', async function() { - const vegaSpec = await PageObjects.visualize.getVegaSpec(); + const vegaSpec = await PageObjects.vegaChart.getSpec(); expect(vegaSpec) .to.contain('{') .and.to.contain('data'); @@ -48,7 +48,7 @@ export default function({ getService, getPageObjects }) { }); it('should have view and control containers', async function() { - const view = await PageObjects.visualize.getVegaViewContainer(); + const view = await PageObjects.vegaChart.getViewContainer(); expect(view).to.be.ok(); const size = await view.getSize(); expect(size) @@ -57,7 +57,7 @@ export default function({ getService, getPageObjects }) { expect(size.width).to.be.above(0); expect(size.height).to.be.above(0); - const controls = await PageObjects.visualize.getVegaControlContainer(); + const controls = await PageObjects.vegaChart.getControlContainer(); expect(controls).to.be.ok(); }); }); @@ -73,9 +73,9 @@ export default function({ getService, getPageObjects }) { }); it.skip('should render different data in response to filter change', async function() { - await PageObjects.visualize.expectVisToMatchScreenshot('vega_chart'); + await PageObjects.vegaChart.expectVisToMatchScreenshot('vega_chart'); await filterBar.addFilter('@tags.raw', 'is', 'error'); - await PageObjects.visualize.expectVisToMatchScreenshot('vega_chart_filtered'); + await PageObjects.vegaChart.expectVisToMatchScreenshot('vega_chart_filtered'); }); }); }); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index 1153cc12e23ed..2efa812c7a734 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -23,8 +23,9 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); const inspector = getService('inspector'); + const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); // FLAKY: https://github.com/elastic/kibana/issues/22322 describe('vertical bar chart', function() { @@ -38,13 +39,13 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = X-Axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); log.debug('Field = @timestamp'); - await PageObjects.visualize.selectField('@timestamp'); + await PageObjects.visEditor.selectField('@timestamp'); // leaving Interval set to Auto - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); }; before(initBarChart); @@ -53,7 +54,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -92,7 +93,7 @@ export default function({ getService, getPageObjects }) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -203,15 +204,15 @@ export default function({ getService, getPageObjects }) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); }); - await PageObjects.visualize.toggleOpenEditor(2); - await PageObjects.visualize.clickDropPartialBuckets(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(2); + await PageObjects.visEditor.clickDropPartialBuckets(); + await PageObjects.visEditor.clickGo(); expectedChartValues = [ 218, @@ -279,7 +280,7 @@ export default function({ getService, getPageObjects }) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -291,13 +292,13 @@ export default function({ getService, getPageObjects }) { const axisId = 'ValueAxis-1'; it('should show ticks on selecting log scale', async () => { - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions(axisId); - await PageObjects.visualize.selectYAxisScaleType(axisId, 'log'); - await PageObjects.visualize.clickYAxisAdvancedOptions(axisId); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); + await PageObjects.visEditor.clickYAxisAdvancedOptions(axisId); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -323,9 +324,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting log scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -351,10 +352,10 @@ export default function({ getService, getPageObjects }) { }); it('should show ticks on selecting square root scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'square root'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '0', '200', @@ -370,18 +371,18 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting square root scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'linear'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); const expectedLabels = [ '0', @@ -398,9 +399,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting linear scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); @@ -409,11 +410,11 @@ export default function({ getService, getPageObjects }) { describe('vertical bar in percent mode', async () => { it('should show ticks with percentage values', async function() { const axisId = 'ValueAxis-1'; - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions(axisId); - await PageObjects.visualize.selectYAxisMode('percentage'); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisMode('percentage'); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); expect(labels[0]).to.eql('0%'); expect(labels[labels.length - 1]).to.eql('100%'); }); @@ -423,36 +424,36 @@ export default function({ getService, getPageObjects }) { before(initBarChart); it('should show correct series', async function() { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('response.raw'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); const expectedEntries = ['200', '404', '503']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); it('should allow custom sorting of series', async () => { - await PageObjects.visualize.toggleOpenEditor(1, 'false'); - await PageObjects.visualize.selectCustomSortMetric(3, 'Min', 'bytes'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(1, 'false'); + await PageObjects.visEditor.selectCustomSortMetric(3, 'Min', 'bytes'); + await PageObjects.visEditor.clickGo(); const expectedEntries = ['404', '200', '503']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); it('should correctly filter by legend', async () => { - await PageObjects.visualize.filterLegend('200'); - await PageObjects.visualize.waitForVisualization(); - const legendEntries = await PageObjects.visualize.getLegendEntries(); + await PageObjects.visChart.filterLegend('200'); + await PageObjects.visChart.waitForVisualization(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); const expectedEntries = ['200']; expect(legendEntries).to.eql(expectedEntries); await filterBar.removeFilter('response.raw'); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); }); @@ -460,18 +461,18 @@ export default function({ getService, getPageObjects }) { before(initBarChart); it('should show correct series', async function() { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('response.raw'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - - await PageObjects.visualize.toggleOpenEditor(3, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('machine.os'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + + await PageObjects.visEditor.toggleOpenEditor(3, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); const expectedEntries = [ '200 - win 8', @@ -490,18 +491,18 @@ export default function({ getService, getPageObjects }) { '404 - win 8', '404 - win xp', ]; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); it('should show correct series when disabling first agg', async function() { // this will avoid issues with the play tooltip covering the disable agg button - await PageObjects.visualize.scrollSubjectIntoView('metricsAggGroup'); - await PageObjects.visualize.toggleDisabledAgg(3); - await PageObjects.visualize.clickGo(); + await testSubjects.scrollIntoView('metricsAggGroup'); + await PageObjects.visEditor.toggleDisabledAgg(3); + await PageObjects.visEditor.clickGo(); const expectedEntries = ['win 8', 'win xp', 'ios', 'osx', 'win 7']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); }); @@ -510,24 +511,24 @@ export default function({ getService, getPageObjects }) { before(initBarChart); it('should show correct series', async function() { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.toggleOpenEditor(1); - await PageObjects.visualize.selectAggregation('Derivative', 'metrics'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.toggleOpenEditor(1); + await PageObjects.visEditor.selectAggregation('Derivative', 'metrics'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); const expectedEntries = ['Derivative of Count']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); it('should show an error if last bucket aggregation is terms', async () => { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('response.raw'); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); - const errorMessage = await PageObjects.visualize.getBucketErrorMessage(); + const errorMessage = await PageObjects.visEditor.getBucketErrorMessage(); expect(errorMessage).to.contain('Last bucket aggregation must be "Date Histogram"'); }); }); diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js index e796f281b0689..2371df6e92476 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js @@ -23,7 +23,7 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); const inspector = getService('inspector'); - const PageObjects = getPageObjects(['common', 'visualize', 'header']); + const PageObjects = getPageObjects(['common', 'visualize', 'header', 'visEditor', 'visChart']); // FLAKY: https://github.com/elastic/kibana/issues/22322 describe.skip('vertical bar chart with index without time filter', function() { @@ -39,13 +39,13 @@ export default function({ getService, getPageObjects }) { ); await PageObjects.common.sleep(500); log.debug('Bucket = X-Axis'); - await PageObjects.visualize.clickBucket('X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); - await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); log.debug('Field = @timestamp'); - await PageObjects.visualize.selectField('@timestamp'); - await PageObjects.visualize.setCustomInterval('3h'); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('3h', { type: 'custom' }); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); }; before(initBarChart); @@ -54,7 +54,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); }); it('should have inspector enabled', async function() { @@ -93,7 +93,7 @@ export default function({ getService, getPageObjects }) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visualize.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -134,13 +134,13 @@ export default function({ getService, getPageObjects }) { const axisId = 'ValueAxis-1'; it('should show ticks on selecting log scale', async () => { - await PageObjects.visualize.clickMetricsAndAxes(); - await PageObjects.visualize.clickYAxisOptions(axisId); - await PageObjects.visualize.selectYAxisScaleType(axisId, 'log'); - await PageObjects.visualize.clickYAxisAdvancedOptions(axisId); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); + await PageObjects.visEditor.clickYAxisAdvancedOptions(axisId); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -166,9 +166,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting log scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '2', '3', @@ -194,10 +194,10 @@ export default function({ getService, getPageObjects }) { }); it('should show ticks on selecting square root scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'square root'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = [ '0', '200', @@ -213,18 +213,18 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting square root scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { - await PageObjects.visualize.selectYAxisScaleType(axisId, 'linear'); - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); const expectedLabels = [ '0', @@ -241,9 +241,9 @@ export default function({ getService, getPageObjects }) { }); it('should show filtered ticks on selecting linear scale', async () => { - await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visualize.clickGo(); - const labels = await PageObjects.visualize.getYAxisLabels(); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); @@ -253,18 +253,18 @@ export default function({ getService, getPageObjects }) { before(initBarChart); it('should show correct series', async function() { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('response.raw'); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); await PageObjects.header.waitUntilLoadingHasFinished(); const expectedEntries = ['200', '404', '503']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); }); @@ -273,20 +273,20 @@ export default function({ getService, getPageObjects }) { before(initBarChart); it('should show correct series', async function() { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('response.raw'); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.visualize.toggleOpenEditor(3, 'false'); - await PageObjects.visualize.clickBucket('Split series'); - await PageObjects.visualize.selectAggregation('Terms'); - await PageObjects.visualize.selectField('machine.os'); + await PageObjects.visEditor.toggleOpenEditor(3, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); await PageObjects.header.waitUntilLoadingHasFinished(); const expectedEntries = [ @@ -306,17 +306,17 @@ export default function({ getService, getPageObjects }) { '404 - win 8', '404 - win xp', ]; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); it('should show correct series when disabling first agg', async function() { - await PageObjects.visualize.toggleDisabledAgg(3); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.toggleDisabledAgg(3); + await PageObjects.visEditor.clickGo(); await PageObjects.header.waitUntilLoadingHasFinished(); const expectedEntries = ['win 8', 'win xp', 'ios', 'osx', 'win 7']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); }); @@ -325,17 +325,17 @@ export default function({ getService, getPageObjects }) { before(initBarChart); it('should show correct series', async function() { - await PageObjects.visualize.toggleOpenEditor(2, 'false'); - await PageObjects.visualize.toggleOpenEditor(1); - await PageObjects.visualize.selectAggregation('Derivative', 'metrics'); + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.toggleOpenEditor(1); + await PageObjects.visEditor.selectAggregation('Derivative', 'metrics'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); await PageObjects.header.waitUntilLoadingHasFinished(); const expectedEntries = ['Derivative of Count']; - const legendEntries = await PageObjects.visualize.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); expect(legendEntries).to.eql(expectedEntries); }); }); diff --git a/test/functional/apps/visualize/_visualize_listing.js b/test/functional/apps/visualize/_visualize_listing.js index 63a8a8310d2c1..e277c3c7d104d 100644 --- a/test/functional/apps/visualize/_visualize_listing.js +++ b/test/functional/apps/visualize/_visualize_listing.js @@ -19,8 +19,9 @@ import expect from '@kbn/expect'; -export default function({ getPageObjects }) { - const PageObjects = getPageObjects(['visualize', 'header', 'common']); +export default function({ getService, getPageObjects }) { + const PageObjects = getPageObjects(['visualize', 'visEditor']); + const listingTable = getService('listingTable'); // FLAKY: https://github.com/elastic/kibana/issues/40912 describe.skip('visualize listing page', function describeIndexTests() { @@ -36,7 +37,7 @@ export default function({ getPageObjects }) { // type markdown is used for simplicity await PageObjects.visualize.createSimpleMarkdownViz(vizName); await PageObjects.visualize.gotoVisualizationLandingPage(); - const visCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + const visCount = await listingTable.getItemsCount('visualize'); expect(visCount).to.equal(1); }); @@ -45,11 +46,11 @@ export default function({ getPageObjects }) { await PageObjects.visualize.createSimpleMarkdownViz(vizName + '2'); await PageObjects.visualize.gotoVisualizationLandingPage(); - let visCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + let visCount = await listingTable.getItemsCount('visualize'); expect(visCount).to.equal(3); await PageObjects.visualize.deleteAllVisualizations(); - visCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + visCount = await listingTable.getItemsCount('visualize'); expect(visCount).to.equal(0); }); }); @@ -60,45 +61,45 @@ export default function({ getPageObjects }) { await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); - await PageObjects.visualize.setMarkdownTxt('HELLO'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setMarkdownTxt('HELLO'); + await PageObjects.visEditor.clickGo(); await PageObjects.visualize.saveVisualization('Hello World'); await PageObjects.visualize.gotoVisualizationLandingPage(); }); it('matches on the first word', async function() { - await PageObjects.visualize.searchForItemWithName('Hello'); - const itemCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + await listingTable.searchForItemWithName('Hello'); + const itemCount = await listingTable.getItemsCount('visualize'); expect(itemCount).to.equal(1); }); it('matches the second word', async function() { - await PageObjects.visualize.searchForItemWithName('World'); - const itemCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + await listingTable.searchForItemWithName('World'); + const itemCount = await listingTable.getItemsCount('visualize'); expect(itemCount).to.equal(1); }); it('matches the second word prefix', async function() { - await PageObjects.visualize.searchForItemWithName('Wor'); - const itemCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + await listingTable.searchForItemWithName('Wor'); + const itemCount = await listingTable.getItemsCount('visualize'); expect(itemCount).to.equal(1); }); it('does not match mid word', async function() { - await PageObjects.visualize.searchForItemWithName('orld'); - const itemCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + await listingTable.searchForItemWithName('orld'); + const itemCount = await listingTable.getItemsCount('visualize'); expect(itemCount).to.equal(0); }); it('is case insensitive', async function() { - await PageObjects.visualize.searchForItemWithName('hello world'); - const itemCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + await listingTable.searchForItemWithName('hello world'); + const itemCount = await listingTable.getItemsCount('visualize'); expect(itemCount).to.equal(1); }); it('is using AND operator', async function() { - await PageObjects.visualize.searchForItemWithName('hello banana'); - const itemCount = await PageObjects.visualize.getCountOfItemsInListingTable(); + await listingTable.searchForItemWithName('hello banana'); + const itemCount = await listingTable.getItemsCount('visualize'); expect(itemCount).to.equal(0); }); }); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index 96d9dae519b51..b56a37218aba5 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.js +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'visualize', 'visEditor', 'header', 'timePicker']); const testSubjects = getService('testSubjects'); const find = getService('find'); const comboBox = getService('comboBox'); @@ -65,7 +65,7 @@ export default function({ getService, getPageObjects }) { it('should create a seperate filter pill for parent control and child control', async () => { await comboBox.set('listControlSelect1', '14.61.182.136'); - await PageObjects.visualize.inputControlSubmit(); + await PageObjects.visEditor.inputControlSubmit(); const hasParentControlFilter = await filterBar.hasFilter('geo.src', 'BR'); expect(hasParentControlFilter).to.equal(true); diff --git a/test/functional/apps/visualize/input_control_vis/dynamic_options.js b/test/functional/apps/visualize/input_control_vis/dynamic_options.js index 2354855f12079..d78c780a728a7 100644 --- a/test/functional/apps/visualize/input_control_vis/dynamic_options.js +++ b/test/functional/apps/visualize/input_control_vis/dynamic_options.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'visualize', 'visEditor', 'header', 'timePicker']); const comboBox = getService('comboBox'); describe('dynamic options', () => { @@ -55,7 +55,7 @@ export default function({ getService, getPageObjects }) { it('should not fetch new options when non-string is filtered', async () => { await comboBox.set('fieldSelect-0', 'clientip'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const initialOptions = await comboBox.getOptionsList('listControlSelect0'); expect( diff --git a/test/functional/apps/visualize/input_control_vis/input_control_options.js b/test/functional/apps/visualize/input_control_vis/input_control_options.js index 1c3f63e94ae75..8e8891ac585b3 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_options.js +++ b/test/functional/apps/visualize/input_control_vis/input_control_options.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'visualize', 'visEditor', 'header', 'timePicker']); const testSubjects = getService('testSubjects'); const inspector = getService('inspector'); const find = getService('find'); @@ -38,11 +38,11 @@ export default function({ getService, getPageObjects }) { 'Jan 1, 2017 @ 00:00:00.000', 'Jan 1, 2017 @ 00:00:00.000' ); - await PageObjects.visualize.clickVisEditorTab('controls'); - await PageObjects.visualize.addInputControl(); + await PageObjects.visEditor.clickVisEditorTab('controls'); + await PageObjects.visEditor.addInputControl(); await comboBox.set('indexPatternSelect-0', 'logstash- '); await comboBox.set('fieldSelect-0', FIELD_NAME); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); }); it('should not have inspector enabled', async function() { @@ -89,7 +89,7 @@ export default function({ getService, getPageObjects }) { }); it('should add filter pill when submit button is clicked', async () => { - await PageObjects.visualize.inputControlSubmit(); + await PageObjects.visEditor.inputControlSubmit(); const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios'); expect(hasFilter).to.equal(true); @@ -98,7 +98,7 @@ export default function({ getService, getPageObjects }) { it('should replace existing filter pill(s) when new item is selected', async () => { await comboBox.clear('listControlSelect0'); await comboBox.set('listControlSelect0', 'osx'); - await PageObjects.visualize.inputControlSubmit(); + await PageObjects.visEditor.inputControlSubmit(); await PageObjects.common.sleep(1000); const hasOldFilter = await filterBar.hasFilter(FIELD_NAME, 'ios'); @@ -117,11 +117,11 @@ export default function({ getService, getPageObjects }) { it('should clear form when Clear button is clicked but not remove filter pill', async () => { await comboBox.set('listControlSelect0', 'ios'); - await PageObjects.visualize.inputControlSubmit(); + await PageObjects.visEditor.inputControlSubmit(); const hasFilterBeforeClearBtnClicked = await filterBar.hasFilter(FIELD_NAME, 'ios'); expect(hasFilterBeforeClearBtnClicked).to.equal(true); - await PageObjects.visualize.inputControlClear(); + await PageObjects.visEditor.inputControlClear(); const hasValue = await comboBox.doesComboBoxHaveSelectedOptions('listControlSelect0'); expect(hasValue).to.equal(false); @@ -130,7 +130,7 @@ export default function({ getService, getPageObjects }) { }); it('should remove filter pill when cleared form is submitted', async () => { - await PageObjects.visualize.inputControlSubmit(); + await PageObjects.visEditor.inputControlSubmit(); const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios'); expect(hasFilter).to.equal(false); }); @@ -138,17 +138,17 @@ export default function({ getService, getPageObjects }) { describe('updateFiltersOnChange is true', () => { before(async () => { - await PageObjects.visualize.clickVisEditorTab('options'); - await PageObjects.visualize.checkSwitch('inputControlEditorUpdateFiltersOnChangeCheckbox'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickVisEditorTab('options'); + await PageObjects.visEditor.checkSwitch('inputControlEditorUpdateFiltersOnChangeCheckbox'); + await PageObjects.visEditor.clickGo(); }); after(async () => { - await PageObjects.visualize.clickVisEditorTab('options'); - await PageObjects.visualize.uncheckSwitch( + await PageObjects.visEditor.clickVisEditorTab('options'); + await PageObjects.visEditor.uncheckSwitch( 'inputControlEditorUpdateFiltersOnChangeCheckbox' ); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); }); it('should not display staging control buttons', async () => { @@ -173,9 +173,9 @@ export default function({ getService, getPageObjects }) { describe('useTimeFilter', () => { it('should use global time filter when getting terms', async () => { - await PageObjects.visualize.clickVisEditorTab('options'); - await PageObjects.visualize.checkCheckbox('inputControlEditorUseTimeFilterCheckbox'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickVisEditorTab('options'); + await testSubjects.setCheckbox('inputControlEditorUseTimeFilterCheckbox', 'check'); + await PageObjects.visEditor.clickGo(); // Expect control to be disabled because no terms could be gathered with time filter applied const input = await find.byCssSelector('[data-test-subj="inputControl0"] input'); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index 2d6550de5dec9..f48ba7b54daf1 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const find = getService('find'); - const { visualize } = getPageObjects(['visualize']); + const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { @@ -35,36 +35,22 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should add filter with scripted field', async () => { - await visualize.addInputControl('range'); - await visualize.setFilterParams({ - indexPattern: 'kibana_sample_data_flights', - field: 'hour_of_day', - }); - await visualize.clickGo(); - await visualize.setFilterRange({ - min: '7', - max: '10', - }); - await visualize.inputControlSubmit(); + await visEditor.addInputControl('range'); + await visEditor.setFilterParams(0, 'kibana_sample_data_flights', 'hour_of_day'); + await visEditor.clickGo(); + await visEditor.setFilterRange(0, '7', '10'); + await visEditor.inputControlSubmit(); const controlFilters = await find.allByCssSelector('[data-test-subj^="filter"]'); expect(controlFilters).to.have.length(1); expect(await controlFilters[0].getVisibleText()).to.equal('hour_of_day: 7 to 10'); }); it('should add filter with price field', async () => { - await visualize.addInputControl('range'); - await visualize.setFilterParams({ - aggNth: 1, - indexPattern: 'kibana_sample_data_flights', - field: 'AvgTicketPrice', - }); - await visualize.clickGo(); - await visualize.setFilterRange({ - aggNth: 1, - min: '400', - max: '999', - }); - await visualize.inputControlSubmit(); + await visEditor.addInputControl('range'); + await visEditor.setFilterParams(1, 'kibana_sample_data_flights', 'AvgTicketPrice'); + await visEditor.clickGo(); + await visEditor.setFilterRange(1, '400', '999'); + await visEditor.inputControlSubmit(); const controlFilters = await find.allByCssSelector('[data-test-subj^="filter"]'); expect(controlFilters).to.have.length(2); expect(await controlFilters[1].getVisibleText()).to.equal('AvgTicketPrice: $400 to $999'); diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 7029fbf9e1350..3ba0f217813f2 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -117,8 +117,16 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { await testSubjects.click('discoverOpenButton'); } + async getChartCanvas() { + return await find.byCssSelector('.echChart canvas:last-of-type'); + } + + async chartCanvasExist() { + return await find.existsByCssSelector('.echChart canvas:last-of-type'); + } + async clickHistogramBar() { - const el = await find.byCssSelector('.echChart canvas:last-of-type'); + const el = await this.getChartCanvas(); await browser .getActions() @@ -128,7 +136,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { } async brushHistogram() { - const el = await find.byCssSelector('.echChart canvas:last-of-type'); + const el = await this.getChartCanvas(); + await browser.dragAndDrop( { location: el, offset: { x: 200, y: 20 } }, { location: el, offset: { x: 400, y: 30 } } @@ -279,7 +288,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { async selectIndexPattern(indexPattern) { await testSubjects.click('indexPattern-switch-link'); await find.clickByCssSelector( - `[data-test-subj="indexPattern-switcher"] [title="${indexPattern}*"]` + `[data-test-subj="indexPattern-switcher"] [title="${indexPattern}"]` ); await PageObjects.header.waitUntilLoadingHasFinished(); } diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 5a104c8d17bf2..5526243ea2bbd 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -39,7 +39,6 @@ import { NewsfeedPageProvider } from './newsfeed_page'; import { PointSeriesPageProvider } from './point_series_page'; // @ts-ignore not TS yet import { SettingsPageProvider } from './settings_page'; -// @ts-ignore not TS yet import { SharePageProvider } from './share_page'; // @ts-ignore not TS yet import { ShieldPageProvider } from './shield_page'; @@ -48,8 +47,12 @@ import { TimePickerPageProvider } from './time_picker'; // @ts-ignore not TS yet import { TimelionPageProvider } from './timelion_page'; import { VisualBuilderPageProvider } from './visual_builder_page'; -// @ts-ignore not TS yet import { VisualizePageProvider } from './visualize_page'; +import { VisualizeEditorPageProvider } from './visualize_editor_page'; +import { VisualizeChartPageProvider } from './visualize_chart_page'; +import { TileMapPageProvider } from './tile_map_page'; +import { TagCloudPageProvider } from './tag_cloud_page'; +import { VegaChartPageProvider } from './vega_chart_page'; export const pageObjects = { common: CommonPageProvider, @@ -70,4 +73,9 @@ export const pageObjects = { timePicker: TimePickerPageProvider, visualBuilder: VisualBuilderPageProvider, visualize: VisualizePageProvider, + visEditor: VisualizeEditorPageProvider, + visChart: VisualizeChartPageProvider, + tileMap: TileMapPageProvider, + tagCloud: TagCloudPageProvider, + vegaChart: VegaChartPageProvider, }; diff --git a/test/functional/page_objects/point_series_page.js b/test/functional/page_objects/point_series_page.js index 9172809eb73e5..74bf07b59bc38 100644 --- a/test/functional/page_objects/point_series_page.js +++ b/test/functional/page_objects/point_series_page.js @@ -23,10 +23,6 @@ export function PointSeriesPageProvider({ getService }) { const find = getService('find'); class PointSeriesVis { - async clickOptions() { - return await testSubjects.click('visEditorTaboptions'); - } - async clickAxisOptions() { return await testSubjects.click('visEditorTabadvanced'); } diff --git a/test/functional/page_objects/share_page.ts b/test/functional/page_objects/share_page.ts index 906effcb54a26..fc8db9b78a03f 100644 --- a/test/functional/page_objects/share_page.ts +++ b/test/functional/page_objects/share_page.ts @@ -19,10 +19,9 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function SharePageProvider({ getService, getPageObjects }: FtrProviderContext) { +export function SharePageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); - const PageObjects = getPageObjects(['visualize', 'common']); const log = getService('log'); class SharePage { @@ -78,7 +77,7 @@ export function SharePageProvider({ getService, getPageObjects }: FtrProviderCon async checkShortenUrl() { const shareForm = await testSubjects.find('shareUrlForm'); - await PageObjects.visualize.checkCheckbox('useShortUrl'); + await testSubjects.setCheckbox('useShortUrl', 'check'); await shareForm.waitForDeletedByCssSelector('.euiLoadingSpinner'); } diff --git a/test/functional/page_objects/tag_cloud_page.ts b/test/functional/page_objects/tag_cloud_page.ts new file mode 100644 index 0000000000000..7d87caa39b2fb --- /dev/null +++ b/test/functional/page_objects/tag_cloud_page.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. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { WebElementWrapper } from '../services/lib/web_element_wrapper'; + +export function TagCloudPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const { header, visChart } = getPageObjects(['header', 'visChart']); + + class TagCloudPage { + public async selectTagCloudTag(tagDisplayText: string) { + await testSubjects.click(tagDisplayText); + await header.waitUntilLoadingHasFinished(); + } + + public async getTextTag() { + await visChart.waitForVisualization(); + const elements = await find.allByCssSelector('text'); + return await Promise.all(elements.map(async element => await element.getVisibleText())); + } + + public async getTextSizes() { + const tags = await find.allByCssSelector('text'); + async function returnTagSize(tag: WebElementWrapper) { + const style = await tag.getAttribute('style'); + const fontSize = style.match(/font-size: ([^;]*);/); + return fontSize ? fontSize[1] : ''; + } + return await Promise.all(tags.map(returnTagSize)); + } + } + + return new TagCloudPage(); +} diff --git a/test/functional/page_objects/tile_map_page.ts b/test/functional/page_objects/tile_map_page.ts new file mode 100644 index 0000000000000..b41578f782af4 --- /dev/null +++ b/test/functional/page_objects/tile_map_page.ts @@ -0,0 +1,102 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function TileMapPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const log = getService('log'); + const inspector = getService('inspector'); + const { header } = getPageObjects(['header']); + + class TileMapPage { + public async getZoomSelectors(zoomSelector: string) { + return await find.allByCssSelector(zoomSelector); + } + + public async clickMapButton(zoomSelector: string, waitForLoading?: boolean) { + await retry.try(async () => { + const zooms = await this.getZoomSelectors(zoomSelector); + await Promise.all(zooms.map(async zoom => await zoom.click())); + if (waitForLoading) { + await header.waitUntilLoadingHasFinished(); + } + }); + } + + public async getVisualizationRequest() { + log.debug('getVisualizationRequest'); + await inspector.open(); + await testSubjects.click('inspectorViewChooser'); + await testSubjects.click('inspectorViewChooserRequests'); + await testSubjects.click('inspectorRequestDetailRequest'); + return await testSubjects.getVisibleText('inspectorRequestBody'); + } + + public async getMapBounds(): Promise { + const request = await this.getVisualizationRequest(); + const requestObject = JSON.parse(request); + return requestObject.aggs.filter_agg.filter.geo_bounding_box['geo.coordinates']; + } + + public async clickMapZoomIn(waitForLoading = true) { + await this.clickMapButton('a.leaflet-control-zoom-in', waitForLoading); + } + + public async clickMapZoomOut(waitForLoading = true) { + await this.clickMapButton('a.leaflet-control-zoom-out', waitForLoading); + } + + public async getMapZoomEnabled(zoomSelector: string): Promise { + const zooms = await this.getZoomSelectors(zoomSelector); + const classAttributes = await Promise.all( + zooms.map(async zoom => await zoom.getAttribute('class')) + ); + return !classAttributes.join('').includes('leaflet-disabled'); + } + + public async zoomAllTheWayOut(): Promise { + // we can tell we're at level 1 because zoom out is disabled + return await retry.try(async () => { + await this.clickMapZoomOut(); + const enabled = await this.getMapZoomOutEnabled(); + // should be able to zoom more as current config has 0 as min level. + if (enabled) { + throw new Error('Not fully zoomed out yet'); + } + }); + } + + public async getMapZoomInEnabled() { + return await this.getMapZoomEnabled('a.leaflet-control-zoom-in'); + } + + public async getMapZoomOutEnabled() { + return await this.getMapZoomEnabled('a.leaflet-control-zoom-out'); + } + + public async clickMapFitDataBounds() { + return await this.clickMapButton('a.fa-crop'); + } + } + + return new TileMapPage(); +} diff --git a/test/functional/page_objects/vega_chart_page.ts b/test/functional/page_objects/vega_chart_page.ts new file mode 100644 index 0000000000000..9931ebebef6ef --- /dev/null +++ b/test/functional/page_objects/vega_chart_page.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function VegaChartPageProvider({ + getService, + getPageObjects, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const screenshot = getService('screenshots'); + const log = getService('log'); + const { visEditor, visChart } = getPageObjects(['visEditor', 'visChart']); + + class VegaChartPage { + public async getSpec() { + // Adapted from console_page.js:getVisibleTextFromAceEditor(). Is there a common utilities file? + const editor = await testSubjects.find('vega-editor'); + const lines = await editor.findAllByClassName('ace_line_group'); + const linesText = await Promise.all( + lines.map(async line => { + return await line.getVisibleText(); + }) + ); + return linesText.join('\n'); + } + + public async getViewContainer() { + return await find.byCssSelector('div.vgaVis__view'); + } + + public async getControlContainer() { + return await find.byCssSelector('div.vgaVis__controls'); + } + + /** + * Removes chrome and takes a small screenshot of a vis to compare against a baseline. + * @param {string} name The name of the baseline image. + * @param {object} opts Options object. + * @param {number} opts.threshold Threshold for allowed variance when comparing images. + */ + public async expectVisToMatchScreenshot(name: string, opts = { threshold: 0.05 }) { + log.debug(`expectVisToMatchScreenshot(${name})`); + + // Collapse sidebar and inject some CSS to hide the nav so we have a focused screenshot + await visEditor.clickEditorSidebarCollapse(); + await visChart.waitForVisualizationRenderingStabilized(); + await browser.execute(` + var el = document.createElement('style'); + el.id = '__data-test-style'; + el.innerHTML = '[data-test-subj="headerGlobalNav"] { display: none; } '; + el.innerHTML += '[data-test-subj="top-nav"] { display: none; } '; + el.innerHTML += '[data-test-subj="experimentalVisInfo"] { display: none; } '; + document.body.appendChild(el); + `); + + const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines); + + // Reset the chart to its original state + await browser.execute(` + var el = document.getElementById('__data-test-style'); + document.body.removeChild(el); + `); + await visEditor.clickEditorSidebarCollapse(); + await visChart.waitForVisualizationRenderingStabilized(); + expect(percentDifference).to.be.lessThan(opts.threshold); + } + } + + return new VegaChartPage(); +} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 97d5787350376..2fa59d5fd89d8 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -26,7 +26,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro const retry = getService('retry'); const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); - const PageObjects = getPageObjects(['common', 'header', 'visualize', 'timePicker']); + const PageObjects = getPageObjects(['common', 'header', 'visualize', 'timePicker', 'visChart']); type Duration = | 'Milliseconds' @@ -101,7 +101,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async getMetricValue() { - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const metricValue = await find.byCssSelector('.tvbVisMetric__value--primary'); return metricValue.getVisibleText(); } @@ -110,7 +110,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea'); await this.clearMarkdown(); await input.type(markdown, { charByChar: true }); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); } public async clearMarkdown() { @@ -304,7 +304,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async getRhythmChartLegendValue(nth = 0) { - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const metricValue = ( await find.allByCssSelector(`.echLegendItem .echLegendItem__displayValue`) )[nth]; @@ -348,7 +348,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro const prevAggs = await testSubjects.findAll('aggSelector'); const elements = await testSubjects.findAll('addMetricAddBtn'); await elements[nth].click(); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); await retry.waitFor('new agg is added', async () => { const currentAggs = await testSubjects.findAll('aggSelector'); return currentAggs.length > prevAggs.length; @@ -485,7 +485,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro await this.checkColorPickerPopUpIsPresent(); await find.setValue('.tvbColorPickerPopUp input', colorHex); await this.clickColorPicker(); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); } public async checkColorPickerPopUpIsPresent(): Promise { @@ -494,10 +494,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async changePanelPreview(nth: number = 0): Promise { - const prevRenderingCount = await PageObjects.visualize.getVisualizationRenderingCount(); + const prevRenderingCount = await PageObjects.visChart.getVisualizationRenderingCount(); const changePreviewBtnArray = await testSubjects.findAll('AddActivatePanelBtn'); await changePreviewBtnArray[nth].click(); - await PageObjects.visualize.waitForRenderingCount(prevRenderingCount + 1); + await PageObjects.visChart.waitForRenderingCount(prevRenderingCount + 1); } public async checkPreviewIsDisabled(): Promise { @@ -508,7 +508,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro public async cloneSeries(nth: number = 0): Promise { const cloneBtnArray = await testSubjects.findAll('AddCloneBtn'); await cloneBtnArray[nth].click(); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); } /** @@ -525,10 +525,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async deleteSeries(nth: number = 0): Promise { - const prevRenderingCount = await PageObjects.visualize.getVisualizationRenderingCount(); + const prevRenderingCount = await PageObjects.visChart.getVisualizationRenderingCount(); const cloneBtnArray = await testSubjects.findAll('AddDeleteBtn'); await cloneBtnArray[nth].click(); - await PageObjects.visualize.waitForRenderingCount(prevRenderingCount + 1); + await PageObjects.visChart.waitForRenderingCount(prevRenderingCount + 1); } public async getLegendItems(): Promise { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts new file mode 100644 index 0000000000000..138e5758ede7c --- /dev/null +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -0,0 +1,386 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const config = getService('config'); + const find = getService('find'); + const log = getService('log'); + const retry = getService('retry'); + const table = getService('table'); + const defaultFindTimeout = config.get('timeouts.find'); + const { common } = getPageObjects(['common']); + + class VisualizeChart { + public async getYAxisTitle() { + const title = await find.byCssSelector('.y-axis-div .y-axis-title text'); + return await title.getVisibleText(); + } + + public async getXAxisLabels() { + const xAxis = await find.byCssSelector('.visAxis--x.visAxis__column--bottom'); + const $ = await xAxis.parseDomContent(); + return $('.x > g > text') + .toArray() + .map(tick => + $(tick) + .text() + .trim() + ); + } + + public async getYAxisLabels() { + const yAxis = await find.byCssSelector('.visAxis__column--y.visAxis__column--left'); + const $ = await yAxis.parseDomContent(); + return $('.y > g > text') + .toArray() + .map(tick => + $(tick) + .text() + .trim() + ); + } + + /** + * Gets the chart data and scales it based on chart height and label. + * @param dataLabel data-label value + * @param axis axis value, 'ValueAxis-1' by default + * + * Returns an array of height values + */ + public async getAreaChartData(dataLabel: string, axis = 'ValueAxis-1') { + const yAxisRatio = await this.getChartYAxisRatio(axis); + + const rectangle = await find.byCssSelector('rect.background'); + const yAxisHeight = Number(await rectangle.getAttribute('height')); + log.debug(`height --------- ${yAxisHeight}`); + + const path = await retry.try( + async () => + await find.byCssSelector(`path[data-label="${dataLabel}"]`, defaultFindTimeout * 2) + ); + const data = await path.getAttribute('d'); + log.debug(data); + // This area chart data starts with a 'M'ove to a x,y location, followed + // by a bunch of 'L'ines from that point to the next. Those points are + // the values we're going to use to calculate the data values we're testing. + // So git rid of the one 'M' and split the rest on the 'L's. + const tempArray = data + .replace('M ', '') + .replace('M', '') + .replace(/ L /g, 'L') + .replace(/ /g, ',') + .split('L'); + const chartSections = tempArray.length / 2; + const chartData = []; + for (let i = 0; i < chartSections; i++) { + chartData[i] = Math.round((yAxisHeight - Number(tempArray[i].split(',')[1])) * yAxisRatio); + log.debug('chartData[i] =' + chartData[i]); + } + return chartData; + } + + /** + * Returns the paths that compose an area chart. + * @param dataLabel data-label value + */ + public async getAreaChartPaths(dataLabel: string) { + const path = await retry.try( + async () => + await find.byCssSelector(`path[data-label="${dataLabel}"]`, defaultFindTimeout * 2) + ); + const data = await path.getAttribute('d'); + log.debug(data); + // This area chart data starts with a 'M'ove to a x,y location, followed + // by a bunch of 'L'ines from that point to the next. Those points are + // the values we're going to use to calculate the data values we're testing. + // So git rid of the one 'M' and split the rest on the 'L's. + return data.split('L'); + } + + /** + * Gets the dots and normalizes their height. + * @param dataLabel data-label value + * @param axis axis value, 'ValueAxis-1' by default + */ + public async getLineChartData(dataLabel = 'Count', axis = 'ValueAxis-1') { + // 1). get the range/pixel ratio + const yAxisRatio = await this.getChartYAxisRatio(axis); + // 2). find and save the y-axis pixel size (the chart height) + const rectangle = await find.byCssSelector('clipPath rect'); + const yAxisHeight = Number(await rectangle.getAttribute('height')); + // 3). get the visWrapper__chart elements + const chartTypes = await retry.try( + async () => + await find.allByCssSelector( + `.visWrapper__chart circle[data-label="${dataLabel}"][fill-opacity="1"]`, + defaultFindTimeout * 2 + ) + ); + // 4). for each chart element, find the green circle, then the cy position + const chartData = await Promise.all( + chartTypes.map(async chart => { + const cy = Number(await chart.getAttribute('cy')); + // the point_series_options test has data in the billions range and + // getting 11 digits of precision with these calculations is very hard + return Math.round(Number(((yAxisHeight - cy) * yAxisRatio).toPrecision(6))); + }) + ); + + return chartData; + } + + /** + * Returns bar chart data in pixels + * @param dataLabel data-label value + * @param axis axis value, 'ValueAxis-1' by default + */ + public async getBarChartData(dataLabel = 'Count', axis = 'ValueAxis-1') { + const yAxisRatio = await this.getChartYAxisRatio(axis); + const svg = await find.byCssSelector('div.chart'); + const $ = await svg.parseDomContent(); + const chartData = $(`g > g.series > rect[data-label="${dataLabel}"]`) + .toArray() + .map(chart => { + const barHeight = Number($(chart).attr('height')); + return Math.round(barHeight * yAxisRatio); + }); + + return chartData; + } + + /** + * Returns the range/pixel ratio + * @param axis axis value, 'ValueAxis-1' by default + */ + private async getChartYAxisRatio(axis = 'ValueAxis-1') { + // 1). get the maximum chart Y-Axis marker value and Y position + const maxYAxisChartMarker = await retry.try( + async () => + await find.byCssSelector( + `div.visAxis__splitAxes--y > div > svg > g.${axis} > g:last-of-type.tick` + ) + ); + const maxYLabel = (await maxYAxisChartMarker.getVisibleText()).replace(/,/g, ''); + const maxYLabelYPosition = (await maxYAxisChartMarker.getPosition()).y; + log.debug(`maxYLabel = ${maxYLabel}, maxYLabelYPosition = ${maxYLabelYPosition}`); + + // 2). get the minimum chart Y-Axis marker value and Y position + const minYAxisChartMarker = await find.byCssSelector( + 'div.visAxis__column--y.visAxis__column--left > div > div > svg:nth-child(2) > g > g:nth-child(1).tick' + ); + const minYLabel = (await minYAxisChartMarker.getVisibleText()).replace(',', ''); + const minYLabelYPosition = (await minYAxisChartMarker.getPosition()).y; + return (Number(maxYLabel) - Number(minYLabel)) / (minYLabelYPosition - maxYLabelYPosition); + } + + public async toggleLegend(show = true) { + await retry.try(async () => { + const isVisible = find.byCssSelector('.visLegend'); + if ((show && !isVisible) || (!show && isVisible)) { + await testSubjects.click('vislibToggleLegend'); + } + }); + } + + public async filterLegend(name: string) { + await this.toggleLegend(); + await testSubjects.click(`legend-${name}`); + const filters = await testSubjects.find(`legend-${name}-filters`); + const [filterIn] = await filters.findAllByCssSelector(`input`); + await filterIn.click(); + await this.waitForVisualizationRenderingStabilized(); + } + + public async doesLegendColorChoiceExist(color: string) { + return await testSubjects.exists(`legendSelectColor-${color}`); + } + + public async selectNewLegendColorChoice(color: string) { + await testSubjects.click(`legendSelectColor-${color}`); + } + + public async doesSelectedLegendColorExist(color: string) { + return await testSubjects.exists(`legendSelectedColor-${color}`); + } + + public async expectError() { + await testSubjects.existOrFail('visLibVisualizeError'); + } + + public async getVisualizationRenderingCount() { + const visualizationLoader = await testSubjects.find('visualizationLoader'); + const renderingCount = await visualizationLoader.getAttribute('data-rendering-count'); + return Number(renderingCount); + } + + public async waitForRenderingCount(minimumCount = 1) { + await retry.waitFor( + `rendering count to be greater than or equal to [${minimumCount}]`, + async () => { + const currentRenderingCount = await this.getVisualizationRenderingCount(); + log.debug(`-- currentRenderingCount=${currentRenderingCount}`); + return currentRenderingCount >= minimumCount; + } + ); + } + + public async waitForVisualizationRenderingStabilized() { + // assuming rendering is done when data-rendering-count is constant within 1000 ms + await retry.waitFor('rendering count to stabilize', async () => { + const firstCount = await this.getVisualizationRenderingCount(); + log.debug(`-- firstCount=${firstCount}`); + + await common.sleep(1000); + + const secondCount = await this.getVisualizationRenderingCount(); + log.debug(`-- secondCount=${secondCount}`); + + return firstCount === secondCount; + }); + } + + public async waitForVisualization() { + await this.waitForVisualizationRenderingStabilized(); + await find.byCssSelector('.visualization'); + } + + public async getLegendEntries() { + const legendEntries = await find.allByCssSelector( + '.visLegend__button', + defaultFindTimeout * 2 + ); + return await Promise.all( + legendEntries.map(async chart => await chart.getAttribute('data-label')) + ); + } + + public async openLegendOptionColors(name: string) { + await this.waitForVisualizationRenderingStabilized(); + await retry.try(async () => { + // This click has been flaky in opening the legend, hence the retry. See + // https://github.com/elastic/kibana/issues/17468 + await testSubjects.click(`legend-${name}`); + await this.waitForVisualizationRenderingStabilized(); + // arbitrary color chosen, any available would do + const isOpen = await this.doesLegendColorChoiceExist('#EF843C'); + if (!isOpen) { + throw new Error('legend color selector not open'); + } + }); + } + + public async filterOnTableCell(column: string, row: string) { + await retry.try(async () => { + const tableVis = await testSubjects.find('tableVis'); + const cell = await tableVis.findByCssSelector( + `tbody tr:nth-child(${row}) td:nth-child(${column})` + ); + await cell.moveMouseTo(); + const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell); + await filterBtn.click(); + }); + } + + public async getMarkdownText() { + const markdownContainer = await testSubjects.find('markdownBody'); + return markdownContainer.getVisibleText(); + } + + public async getMarkdownBodyDescendentText(selector: string) { + const markdownContainer = await testSubjects.find('markdownBody'); + const element = await find.descendantDisplayedByCssSelector(selector, markdownContainer); + return element.getVisibleText(); + } + + /** + * If you are writing new tests, you should rather look into getTableVisContent method instead. + */ + public async getTableVisData() { + return await testSubjects.getVisibleText('paginated-table-body'); + } + + /** + * This function is the newer function to retrieve data from within a table visualization. + * It uses a better return format, than the old getTableVisData, by properly splitting + * cell values into arrays. Please use this function for newer tests. + */ + public async getTableVisContent({ stripEmptyRows = true } = {}) { + return await retry.try(async () => { + const container = await testSubjects.find('tableVis'); + const allTables = await testSubjects.findAllDescendant('paginated-table-body', container); + + if (allTables.length === 0) { + return []; + } + + const allData = await Promise.all( + allTables.map(async t => { + let data = await table.getDataFromElement(t); + if (stripEmptyRows) { + data = data.filter(row => row.length > 0 && row.some(cell => cell.trim().length > 0)); + } + return data; + }) + ); + + if (allTables.length === 1) { + // If there was only one table we return only the data for that table + // This prevents an unnecessary array around that single table, which + // is the case we have in most tests. + return allData[0]; + } + + return allData; + }); + } + + public async getMetric() { + const elements = await find.allByCssSelector( + '[data-test-subj="visualizationLoader"] .mtrVis__container' + ); + const values = await Promise.all( + elements.map(async element => { + const text = await element.getVisibleText(); + return text; + }) + ); + return values + .filter(item => item.length > 0) + .reduce((arr: string[], item) => arr.concat(item.split('\n')), []); + } + + public async getGaugeValue() { + const elements = await find.allByCssSelector( + '[data-test-subj="visualizationLoader"] .chart svg text' + ); + const values = await Promise.all( + elements.map(async element => { + const text = await element.getVisibleText(); + return text; + }) + ); + return values.filter(item => item.length > 0); + } + } + + return new VisualizeChart(); +} diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts new file mode 100644 index 0000000000000..7e512975356f3 --- /dev/null +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -0,0 +1,462 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const log = getService('log'); + const retry = getService('retry'); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + const { common, header, visChart } = getPageObjects(['common', 'header', 'visChart']); + + interface IntervalOptions { + type?: 'default' | 'numeric' | 'custom'; + aggNth?: number; + append?: boolean; + } + + class VisualizeEditorPage { + public async clickDataTab() { + await testSubjects.click('visualizeEditDataLink'); + } + + public async clickOptionsTab() { + await testSubjects.click('visEditorTaboptions'); + } + + public async clickMetricsAndAxes() { + await testSubjects.click('visEditorTabadvanced'); + } + + public async clickVisEditorTab(tabName: string) { + await testSubjects.click('visEditorTab' + tabName); + await header.waitUntilLoadingHasFinished(); + } + + public async addInputControl(type?: string) { + if (type) { + const selectInput = await testSubjects.find('selectControlType'); + await selectInput.type(type); + } + await testSubjects.click('inputControlEditorAddBtn'); + await header.waitUntilLoadingHasFinished(); + } + + public async inputControlClear() { + await testSubjects.click('inputControlClearBtn'); + await header.waitUntilLoadingHasFinished(); + } + + public async inputControlSubmit() { + await testSubjects.clickWhenNotDisabled('inputControlSubmitBtn'); + await visChart.waitForVisualizationRenderingStabilized(); + } + + public async clickGo() { + const prevRenderingCount = await visChart.getVisualizationRenderingCount(); + log.debug(`Before Rendering count ${prevRenderingCount}`); + await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton'); + await visChart.waitForRenderingCount(prevRenderingCount + 1); + } + + public async removeDimension(aggNth: number) { + await testSubjects.click(`visEditorAggAccordion${aggNth} > removeDimensionBtn`); + } + + public async setFilterParams(aggNth: number, indexPattern: string, field: string) { + await comboBox.set(`indexPatternSelect-${aggNth}`, indexPattern); + await comboBox.set(`fieldSelect-${aggNth}`, field); + } + + public async setFilterRange(aggNth: number, min: string, max: string) { + const control = await testSubjects.find(`inputControl${aggNth}`); + const inputMin = await control.findByCssSelector('[name$="minValue"]'); + await inputMin.type(min); + const inputMax = await control.findByCssSelector('[name$="maxValue"]'); + await inputMax.type(max); + } + + public async clickSplitDirection(direction: string) { + const control = await testSubjects.find('visEditorSplitBy'); + const radioBtn = await control.findByCssSelector(`[title="${direction}"]`); + await radioBtn.click(); + } + + /** + * Adds new bucket + * @param bucketName bucket name, like 'X-axis', 'Split rows', 'Split series' + * @param type aggregation type, like 'buckets', 'metrics' + */ + public async clickBucket(bucketName: string, type = 'buckets') { + await testSubjects.click(`visEditorAdd_${type}`); + await find.clickByCssSelector(`[data-test-subj="visEditorAdd_${type}_${bucketName}"`); + } + + public async clickEnableCustomRanges() { + await testSubjects.click('heatmapUseCustomRanges'); + } + + public async clickAddRange() { + await testSubjects.click(`heatmapColorRange__addRangeButton`); + } + + public async setCustomRangeByIndex(index: string, from: string, to: string) { + await testSubjects.setValue(`heatmapColorRange${index}__from`, from); + await testSubjects.setValue(`heatmapColorRange${index}__to`, to); + } + + public async changeHeatmapColorNumbers(value = 6) { + const input = await testSubjects.find(`heatmapColorsNumber`); + await input.clearValueWithKeyboard(); + await input.type(`${value}`); + } + + public async getBucketErrorMessage() { + const error = await find.byCssSelector( + '[group-name="buckets"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText' + ); + const errorMessage = await error.getAttribute('innerText'); + log.debug(errorMessage); + return errorMessage; + } + + public async addNewFilterAggregation() { + await testSubjects.click('visEditorAddFilterButton'); + } + + public async selectField( + fieldValue: string, + groupName = 'buckets', + childAggregationType = false + ) { + log.debug(`selectField ${fieldValue}`); + const selector = ` + [group-name="${groupName}"] + [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen + [data-test-subj="visAggEditorParams"] + ${childAggregationType ? '.visEditorAgg__subAgg' : ''} + [data-test-subj="visDefaultEditorField"] + `; + const fieldEl = await find.byCssSelector(selector); + await comboBox.setElement(fieldEl, fieldValue); + } + + public async selectOrderByMetric(aggNth: number, metric: string) { + const sortSelect = await testSubjects.find(`visEditorOrderBy${aggNth}`); + const sortMetric = await sortSelect.findByCssSelector(`option[value="${metric}"]`); + await sortMetric.click(); + } + + public async selectCustomSortMetric(aggNth: number, metric: string, field: string) { + await this.selectOrderByMetric(aggNth, 'custom'); + await this.selectAggregation(metric, 'buckets', true); + await this.selectField(field, 'buckets', true); + } + + public async selectAggregation( + aggValue: string, + groupName = 'buckets', + childAggregationType = false + ) { + const comboBoxElement = await find.byCssSelector(` + [group-name="${groupName}"] + [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen + ${childAggregationType ? '.visEditorAgg__subAgg' : ''} + [data-test-subj="defaultEditorAggSelect"] + `); + + await comboBox.setElement(comboBoxElement, aggValue); + await common.sleep(500); + } + + /** + * Set the test for a filter aggregation. + * @param {*} filterValue the string value of the filter + * @param {*} filterIndex used when multiple filters are configured on the same aggregation + * @param {*} aggregationId the ID if the aggregation. On Tests, it start at from 2 + */ + public async setFilterAggregationValue( + filterValue: string, + filterIndex = 0, + aggregationId = 2 + ) { + await testSubjects.setValue( + `visEditorFilterInput_${aggregationId}_${filterIndex}`, + filterValue + ); + } + + public async setValue(newValue: string) { + const input = await find.byCssSelector('[data-test-subj="visEditorPercentileRanks"] input'); + await input.clearValue(); + await input.type(newValue); + } + + public async clickEditorSidebarCollapse() { + await testSubjects.click('collapseSideBarButton'); + } + + public async clickDropPartialBuckets() { + await testSubjects.click('dropPartialBucketsCheckbox'); + } + + public async setMarkdownTxt(markdownTxt: string) { + const input = await testSubjects.find('markdownTextarea'); + await input.clearValue(); + await input.type(markdownTxt); + } + + public async isSwitchChecked(selector: string) { + const checkbox = await testSubjects.find(selector); + const isChecked = await checkbox.getAttribute('aria-checked'); + return isChecked === 'true'; + } + + public async checkSwitch(selector: string) { + const isChecked = await this.isSwitchChecked(selector); + if (!isChecked) { + log.debug(`checking switch ${selector}`); + await testSubjects.click(selector); + } + } + + public async uncheckSwitch(selector: string) { + const isChecked = await this.isSwitchChecked(selector); + if (isChecked) { + log.debug(`unchecking switch ${selector}`); + await testSubjects.click(selector); + } + } + + public async setIsFilteredByCollarCheckbox(value = true) { + await retry.try(async () => { + const isChecked = await this.isSwitchChecked('isFilteredByCollarCheckbox'); + if (isChecked !== value) { + await testSubjects.click('isFilteredByCollarCheckbox'); + throw new Error('isFilteredByCollar not set correctly'); + } + }); + } + + public async setCustomLabel(label: string, index = 1) { + const customLabel = await testSubjects.find(`visEditorStringInput${index}customLabel`); + customLabel.type(label); + } + + public async selectYAxisAggregation(agg: string, field: string, label: string, index = 1) { + // index starts on the first "count" metric at 1 + // Each new metric or aggregation added to a visualization gets the next index. + // So to modify a metric or aggregation tests need to keep track of the + // order they are added. + await this.toggleOpenEditor(index); + + // select our agg + const aggSelect = await find.byCssSelector( + `#visEditorAggAccordion${index} [data-test-subj="defaultEditorAggSelect"]` + ); + await comboBox.setElement(aggSelect, agg); + + const fieldSelect = await find.byCssSelector( + `#visEditorAggAccordion${index} [data-test-subj="visDefaultEditorField"]` + ); + // select our field + await comboBox.setElement(fieldSelect, field); + // enter custom label + await this.setCustomLabel(label, index); + } + + public async getField() { + return await comboBox.getComboBoxSelectedOptions('visDefaultEditorField'); + } + + public async sizeUpEditor() { + await testSubjects.click('visualizeEditorResizer'); + await browser.pressKeys(browser.keys.ARROW_RIGHT); + } + + public async toggleDisabledAgg(agg: string) { + await testSubjects.click(`visEditorAggAccordion${agg} > ~toggleDisableAggregationBtn`); + await header.waitUntilLoadingHasFinished(); + } + + public async toggleAggregationEditor(agg: string) { + await find.clickByCssSelector( + `[data-test-subj="visEditorAggAccordion${agg}"] .euiAccordion__button` + ); + await header.waitUntilLoadingHasFinished(); + } + + public async toggleOtherBucket(agg = 2) { + await testSubjects.click(`visEditorAggAccordion${agg} > otherBucketSwitch`); + } + + public async toggleMissingBucket(agg = 2) { + await testSubjects.click(`visEditorAggAccordion${agg} > missingBucketSwitch`); + } + + public async toggleScaleMetrics() { + await testSubjects.click('scaleMetricsSwitch'); + } + + public async toggleAutoMode() { + await testSubjects.click('visualizeEditorAutoButton'); + } + + public async isApplyEnabled() { + const applyButton = await testSubjects.find('visualizeEditorRenderButton'); + return await applyButton.isEnabled(); + } + + public async toggleAccordion(id: string, toState = 'true') { + const toggle = await find.byCssSelector(`button[aria-controls="${id}"]`); + const toggleOpen = await toggle.getAttribute('aria-expanded'); + log.debug(`toggle ${id} expand = ${toggleOpen}`); + if (toggleOpen !== toState) { + log.debug(`toggle ${id} click()`); + await toggle.click(); + } + } + + public async toggleOpenEditor(index: number, toState = 'true') { + // index, see selectYAxisAggregation + await this.toggleAccordion(`visEditorAggAccordion${index}`, toState); + } + + public async toggleAdvancedParams(aggId: string) { + const accordion = await testSubjects.find(`advancedParams-${aggId}`); + const accordionButton = await find.descendantDisplayedByCssSelector('button', accordion); + await accordionButton.click(); + } + + public async clickReset() { + await testSubjects.click('visualizeEditorResetButton'); + await visChart.waitForVisualization(); + } + + public async clickYAxisOptions(axisId: string) { + await testSubjects.click(`toggleYAxisOptions-${axisId}`); + } + + public async clickYAxisAdvancedOptions(axisId: string) { + await testSubjects.click(`toggleYAxisAdvancedOptions-${axisId}`); + } + + public async changeYAxisFilterLabelsCheckbox(axisId: string, enabled: boolean) { + const selector = `yAxisFilterLabelsCheckbox-${axisId}`; + await testSubjects.setCheckbox(selector, enabled ? 'check' : 'uncheck'); + } + + public async setSize(newValue: string, aggId: string) { + const dataTestSubj = aggId + ? `visEditorAggAccordion${aggId} > sizeParamEditor` + : 'sizeParamEditor'; + await testSubjects.setValue(dataTestSubj, String(newValue)); + } + + public async selectChartMode(mode: string) { + const selector = await find.byCssSelector(`#seriesMode0 > option[value="${mode}"]`); + await selector.click(); + } + + public async selectYAxisScaleType(axisId: string, scaleType: string) { + const selectElement = await testSubjects.find(`scaleSelectYAxis-${axisId}`); + const selector = await selectElement.findByCssSelector(`option[value="${scaleType}"]`); + await selector.click(); + } + + public async selectYAxisMode(mode: string) { + const selector = await find.byCssSelector(`#valueAxisMode0 > option[value="${mode}"]`); + await selector.click(); + } + + public async setAxisExtents(min: string, max: string, axisId = 'ValueAxis-1') { + await this.toggleAccordion(`yAxisAccordion${axisId}`); + await this.toggleAccordion(`yAxisOptionsAccordion${axisId}`); + + await testSubjects.click('yAxisSetYExtents'); + await testSubjects.setValue('yAxisYExtentsMax', max); + await testSubjects.setValue('yAxisYExtentsMin', min); + } + + public async selectAggregateWith(fieldValue: string) { + await testSubjects.selectValue('visDefaultEditorAggregateWith', fieldValue); + } + + public async setInterval(newValue: string, options: IntervalOptions = {}) { + const { type = 'default', aggNth = 2, append = false } = options; + log.debug(`visEditor.setInterval(${newValue}, {${type}, ${aggNth}, ${append}})`); + if (type === 'default') { + await comboBox.set('visEditorInterval', newValue); + } else if (type === 'custom') { + await comboBox.setCustom('visEditorInterval', newValue); + } else { + if (append) { + await testSubjects.append(`visEditorInterval${aggNth}`, String(newValue)); + } else { + await testSubjects.setValue(`visEditorInterval${aggNth}`, String(newValue)); + } + } + } + + public async getInterval() { + return await comboBox.getComboBoxSelectedOptions('visEditorInterval'); + } + + public async getNumericInterval(agg = 2) { + return await testSubjects.getAttribute(`visEditorInterval${agg}`, 'value'); + } + + public async clickMetricEditor() { + await find.clickByCssSelector('[group-name="metrics"] .euiAccordion__button'); + } + + public async clickMetricByIndex(index: number) { + const metrics = await find.allByCssSelector( + '[data-test-subj="visualizationLoader"] .mtrVis .mtrVis__container' + ); + expect(metrics.length).greaterThan(index); + await metrics[index].click(); + } + + public async setSelectByOptionText(selectId: string, optionText: string) { + const selectField = await find.byCssSelector(`#${selectId}`); + const options = await find.allByCssSelector(`#${selectId} > option`); + const $ = await selectField.parseDomContent(); + const optionsText = $('option') + .toArray() + .map(option => $(option).text()); + const optionIndex = optionsText.indexOf(optionText); + + if (optionIndex === -1) { + throw new Error( + `Unable to find option '${optionText}' in select ${selectId}. Available options: ${optionsText.join( + ',' + )}` + ); + } + await options[optionIndex].click(); + } + } + + return new VisualizeEditorPage(); +} diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js deleted file mode 100644 index c1ea8be9be98b..0000000000000 --- a/test/functional/page_objects/visualize_page.js +++ /dev/null @@ -1,1402 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { VisualizeConstants } from '../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; -import Bluebird from 'bluebird'; -import expect from '@kbn/expect'; - -export function VisualizePageProvider({ getService, getPageObjects, updateBaselines }) { - const browser = getService('browser'); - const config = getService('config'); - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const find = getService('find'); - const log = getService('log'); - const inspector = getService('inspector'); - const screenshot = getService('screenshots'); - const table = getService('table'); - const globalNav = getService('globalNav'); - const PageObjects = getPageObjects(['common', 'header']); - const defaultFindTimeout = config.get('timeouts.find'); - const comboBox = getService('comboBox'); - - class VisualizePage { - get index() { - return { - LOGSTASH_TIME_BASED: 'logstash-*', - LOGSTASH_NON_TIME_BASED: 'logstash*', - }; - } - - async gotoVisualizationLandingPage() { - log.debug('gotoVisualizationLandingPage'); - await PageObjects.common.navigateToApp('visualize'); - } - - async checkListingSelectAllCheckbox() { - const element = await testSubjects.find('checkboxSelectAll'); - const isSelected = await element.isSelected(); - if (!isSelected) { - log.debug(`checking checkbox "checkboxSelectAll"`); - await testSubjects.click('checkboxSelectAll'); - } - } - - async navigateToNewVisualization() { - log.debug('navigateToApp visualize'); - await PageObjects.common.navigateToApp('visualize'); - await this.clickNewVisualization(); - await this.waitForVisualizationSelectPage(); - } - - async clickNewVisualization() { - // newItemButton button is only visible when there are items in the listing table is displayed. - let exists = await testSubjects.exists('newItemButton'); - if (exists) { - return await testSubjects.click('newItemButton'); - } - - exists = await testSubjects.exists('createVisualizationPromptButton'); - // no viz exist, click createVisualizationPromptButton to create new dashboard - return await this.createVisualizationPromptButton(); - } - - /* - This method should use retry loop to delete visualizations from multiple pages until we find the createVisualizationPromptButton. - Perhaps it *could* set the page size larger than the default 10, but it might still need to loop anyway. - */ - async deleteAllVisualizations() { - await retry.try(async () => { - await this.checkListingSelectAllCheckbox(); - await this.clickDeleteSelected(); - await PageObjects.common.clickConfirmOnModal(); - await testSubjects.find('createVisualizationPromptButton'); - }); - } - - async createSimpleMarkdownViz(vizName) { - await this.gotoVisualizationLandingPage(); - await this.navigateToNewVisualization(); - await this.clickMarkdownWidget(); - await this.setMarkdownTxt(vizName); - await this.clickGo(); - await this.saveVisualization(vizName); - } - - async createVisualizationPromptButton() { - await testSubjects.click('createVisualizationPromptButton'); - } - - async getSearchFilter() { - const searchFilter = await find.allByCssSelector('.euiFieldSearch'); - return searchFilter[0]; - } - - async clearFilter() { - const searchFilter = await this.getSearchFilter(); - await searchFilter.clearValue(); - await searchFilter.click(); - } - - async searchForItemWithName(name) { - log.debug(`searchForItemWithName: ${name}`); - - await retry.try(async () => { - const searchFilter = await this.getSearchFilter(); - await searchFilter.clearValue(); - await searchFilter.click(); - // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. - await searchFilter.type(name.replace('-', ' ')); - await PageObjects.common.pressEnterKey(); - }); - - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async clickDeleteSelected() { - await testSubjects.click('deleteSelectedItems'); - } - - async getCreatePromptExists() { - log.debug('getCreatePromptExists'); - return await testSubjects.exists('createVisualizationPromptButton'); - } - - async getCountOfItemsInListingTable() { - const elements = await find.allByCssSelector('[data-test-subj^="visListingTitleLink"]'); - return elements.length; - } - - async waitForVisualizationSelectPage() { - await retry.try(async () => { - const visualizeSelectTypePage = await testSubjects.find('visNewDialogTypes'); - if (!(await visualizeSelectTypePage.isDisplayed())) { - throw new Error('wait for visualization select page'); - } - }); - } - - async clickVisType(type) { - await testSubjects.click(`visType-${type}`); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async clickAreaChart() { - await this.clickVisType('area'); - } - - async clickDataTable() { - await this.clickVisType('table'); - } - - async clickLineChart() { - await this.clickVisType('line'); - } - - async clickRegionMap() { - await this.clickVisType('region_map'); - } - - async clickMarkdownWidget() { - await this.clickVisType('markdown'); - } - - // clickBucket(bucketName) 'X-axis', 'Split area', 'Split chart' - async clickBucket(bucketName, type = 'buckets') { - await testSubjects.click(`visEditorAdd_${type}`); - await find.clickByCssSelector(`[data-test-subj="visEditorAdd_${type}_${bucketName}"`); - } - - async clickMetric() { - await this.clickVisType('metric'); - } - - async clickGauge() { - await this.clickVisType('gauge'); - } - - async clickPieChart() { - await this.clickVisType('pie'); - } - - async clickTileMap() { - await this.clickVisType('tile_map'); - } - - async clickTagCloud() { - await this.clickVisType('tagcloud'); - } - - async clickVega() { - await this.clickVisType('vega'); - } - - async clickVisualBuilder() { - await this.clickVisType('metrics'); - } - - async clickEditorSidebarCollapse() { - await testSubjects.click('collapseSideBarButton'); - } - - async selectTagCloudTag(tagDisplayText) { - await testSubjects.click(tagDisplayText); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async getTextTag() { - await this.waitForVisualization(); - const elements = await find.allByCssSelector('text'); - return await Promise.all(elements.map(async element => await element.getVisibleText())); - } - - async getTextSizes() { - const tags = await find.allByCssSelector('text'); - async function returnTagSize(tag) { - const style = await tag.getAttribute('style'); - return style.match(/font-size: ([^;]*);/)[1]; - } - return await Promise.all(tags.map(returnTagSize)); - } - - async clickVerticalBarChart() { - await this.clickVisType('histogram'); - } - - async clickHeatmapChart() { - await this.clickVisType('heatmap'); - } - - async clickInputControlVis() { - await this.clickVisType('input_control_vis'); - } - - async getChartTypes() { - const chartTypeField = await testSubjects.find('visNewDialogTypes'); - const chartTypes = await chartTypeField.findAllByTagName('button'); - async function getChartType(chart) { - const label = await testSubjects.findDescendant('visTypeTitle', chart); - return await label.getVisibleText(); - } - const getChartTypesPromises = chartTypes.map(getChartType); - return await Promise.all(getChartTypesPromises); - } - - async selectVisSourceIfRequired() { - log.debug('selectVisSourceIfRequired'); - const selectPage = await testSubjects.findAll('visualizeSelectSearch'); - if (selectPage.length) { - log.debug('a search is required for this visualization'); - await this.clickNewSearch(); - } - } - - async isBetaInfoShown() { - return await testSubjects.exists('betaVisInfo'); - } - - async getBetaTypeLinks() { - return await find.allByCssSelector('[data-vis-stage="beta"]'); - } - - async getExperimentalTypeLinks() { - return await find.allByCssSelector('[data-vis-stage="experimental"]'); - } - - async isExperimentalInfoShown() { - return await testSubjects.exists('experimentalVisInfo'); - } - - async getExperimentalInfo() { - return await testSubjects.find('experimentalVisInfo'); - } - - async clickAbsoluteButton() { - await find.clickByCssSelector( - 'ul.nav.nav-pills.nav-stacked.kbn-timepicker-modes:contains("absolute")', - defaultFindTimeout * 2 - ); - } - - async clickDropPartialBuckets() { - return await testSubjects.click('dropPartialBucketsCheckbox'); - } - - async setMarkdownTxt(markdownTxt) { - const input = await testSubjects.find('markdownTextarea'); - await input.clearValue(); - await input.type(markdownTxt); - } - - async getMarkdownText() { - const markdownContainer = await testSubjects.find('markdownBody'); - return markdownContainer.getVisibleText(); - } - - async getMarkdownBodyDescendentText(selector) { - const markdownContainer = await testSubjects.find('markdownBody'); - const element = await find.descendantDisplayedByCssSelector(selector, markdownContainer); - return element.getVisibleText(); - } - - async getVegaSpec() { - // Adapted from console_page.js:getVisibleTextFromAceEditor(). Is there a common utilities file? - const editor = await testSubjects.find('vega-editor'); - const lines = await editor.findAllByClassName('ace_line_group'); - const linesText = await Bluebird.map(lines, l => l.getVisibleText()); - return linesText.join('\n'); - } - - async getVegaViewContainer() { - return await find.byCssSelector('div.vgaVis__view'); - } - - async getVegaControlContainer() { - return await find.byCssSelector('div.vgaVis__controls'); - } - - async addInputControl(type) { - if (type) { - const selectInput = await testSubjects.find('selectControlType'); - await selectInput.type(type); - } - await testSubjects.click('inputControlEditorAddBtn'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async inputControlSubmit() { - await testSubjects.clickWhenNotDisabled('inputControlSubmitBtn'); - await this.waitForVisualizationRenderingStabilized(); - } - - async inputControlClear() { - await testSubjects.click('inputControlClearBtn'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async isChecked(selector) { - const checkbox = await testSubjects.find(selector); - return await checkbox.isSelected(); - } - - async checkCheckbox(selector) { - const isChecked = await this.isChecked(selector); - if (!isChecked) { - log.debug(`checking checkbox ${selector}`); - await testSubjects.click(selector); - } - } - - async uncheckCheckbox(selector) { - const isChecked = await this.isChecked(selector); - if (isChecked) { - log.debug(`unchecking checkbox ${selector}`); - await testSubjects.click(selector); - } - } - - async isSwitchChecked(selector) { - const checkbox = await testSubjects.find(selector); - const isChecked = await checkbox.getAttribute('aria-checked'); - return isChecked === 'true'; - } - - async checkSwitch(selector) { - const isChecked = await this.isSwitchChecked(selector); - if (!isChecked) { - log.debug(`checking switch ${selector}`); - await testSubjects.click(selector); - } - } - - async uncheckSwitch(selector) { - const isChecked = await this.isSwitchChecked(selector); - if (isChecked) { - log.debug(`unchecking switch ${selector}`); - await testSubjects.click(selector); - } - } - - async setSelectByOptionText(selectId, optionText) { - const selectField = await find.byCssSelector(`#${selectId}`); - const options = await find.allByCssSelector(`#${selectId} > option`); - const $ = await selectField.parseDomContent(); - const optionsText = $('option') - .toArray() - .map(option => $(option).text()); - const optionIndex = optionsText.indexOf(optionText); - - if (optionIndex === -1) { - throw new Error( - `Unable to find option '${optionText}' in select ${selectId}. Available options: ${optionsText.join( - ',' - )}` - ); - } - await options[optionIndex].click(); - } - - async getSideEditorExists() { - return await find.existsByCssSelector('.collapsible-sidebar'); - } - - async setInspectorTablePageSize(size) { - const panel = await testSubjects.find('inspectorPanel'); - await find.clickByButtonText('Rows per page: 20', panel); - // The buttons for setting table page size are in a popover element. This popover - // element appears as if it's part of the inspectorPanel but it's really attached - // to the body element by a portal. - const tableSizesPopover = await find.byCssSelector('.euiPanel'); - await find.clickByButtonText(`${size} rows`, tableSizesPopover); - } - - async getMetric() { - const elements = await find.allByCssSelector( - '[data-test-subj="visualizationLoader"] .mtrVis__container' - ); - const values = await Promise.all( - elements.map(async element => { - const text = await element.getVisibleText(); - return text; - }) - ); - return values - .filter(item => item.length > 0) - .reduce((arr, item) => arr.concat(item.split('\n')), []); - } - - async getGaugeValue() { - const elements = await find.allByCssSelector( - '[data-test-subj="visualizationLoader"] .chart svg text' - ); - const values = await Promise.all( - elements.map(async element => { - const text = await element.getVisibleText(); - return text; - }) - ); - return values.filter(item => item.length > 0); - } - - async clickMetricEditor() { - await find.clickByCssSelector('[group-name="metrics"] .euiAccordion__button'); - } - - async clickMetricByIndex(index) { - log.debug(`clickMetricByIndex(${index})`); - const metrics = await find.allByCssSelector( - '[data-test-subj="visualizationLoader"] .mtrVis .mtrVis__container' - ); - expect(metrics.length).greaterThan(index); - await metrics[index].click(); - } - - async clickNewSearch(indexPattern = this.index.LOGSTASH_TIME_BASED) { - await testSubjects.click(`savedObjectTitle${indexPattern.split(' ').join('-')}`); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async clickSavedSearch(savedSearchName) { - await testSubjects.click(`savedObjectTitle${savedSearchName.split(' ').join('-')}`); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async clickUnlinkSavedSearch() { - await testSubjects.doubleClick('unlinkSavedSearch'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async setValue(newValue) { - const input = await find.byCssSelector('[data-test-subj="visEditorPercentileRanks"] input'); - await input.clearValue(); - await input.type(newValue); - } - - async selectSearch(searchName) { - await find.clickByLinkText(searchName); - } - - async getErrorMessage() { - const element = await find.byCssSelector('.item>h4'); - return await element.getVisibleText(); - } - - async selectAggregation(myString, groupName = 'buckets', childAggregationType = null) { - const comboBoxElement = await find.byCssSelector(` - [group-name="${groupName}"] - [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen - ${childAggregationType ? '.visEditorAgg__subAgg' : ''} - [data-test-subj="defaultEditorAggSelect"] - `); - - await comboBox.setElement(comboBoxElement, myString); - await PageObjects.common.sleep(500); - } - - async applyFilters() { - return await testSubjects.click('filterBarApplyFilters'); - } - /** - * Set the test for a filter aggregation. - * @param {*} filterValue the string value of the filter - * @param {*} filterIndex used when multiple filters are configured on the same aggregation - * @param {*} aggregationId the ID if the aggregation. On Tests, it start at from 2 - */ - async setFilterAggregationValue(filterValue, filterIndex = 0, aggregationId = 2) { - await testSubjects.setValue( - `visEditorFilterInput_${aggregationId}_${filterIndex}`, - filterValue - ); - } - - async addNewFilterAggregation() { - return await testSubjects.click('visEditorAddFilterButton'); - } - - async toggleOpenEditor(index, toState = 'true') { - // index, see selectYAxisAggregation - await this.toggleAccordion(`visEditorAggAccordion${index}`, toState); - } - - async toggleAccordion(id, toState = 'true') { - const toggle = await find.byCssSelector(`button[aria-controls="${id}"]`); - const toggleOpen = await toggle.getAttribute('aria-expanded'); - log.debug(`toggle ${id} expand = ${toggleOpen}`); - if (toggleOpen !== toState) { - log.debug(`toggle ${id} click()`); - await toggle.click(); - } - } - - async toggleAdvancedParams(aggId) { - const accordion = await testSubjects.find(`advancedParams-${aggId}`); - const accordionButton = await find.descendantDisplayedByCssSelector('button', accordion); - await accordionButton.click(); - } - - async selectYAxisAggregation(agg, field, label, index = 1) { - // index starts on the first "count" metric at 1 - // Each new metric or aggregation added to a visualization gets the next index. - // So to modify a metric or aggregation tests need to keep track of the - // order they are added. - await this.toggleOpenEditor(index); - - // select our agg - const aggSelect = await find.byCssSelector( - `#visEditorAggAccordion${index} [data-test-subj="defaultEditorAggSelect"]` - ); - await comboBox.setElement(aggSelect, agg); - - const fieldSelect = await find.byCssSelector( - `#visEditorAggAccordion${index} [data-test-subj="visDefaultEditorField"]` - ); - // select our field - await comboBox.setElement(fieldSelect, field); - // enter custom label - await this.setCustomLabel(label, index); - } - - async setCustomLabel(label, index = 1) { - const customLabel = await testSubjects.find(`visEditorStringInput${index}customLabel`); - customLabel.type(label); - } - - async setAxisExtents(min, max, axisId = 'ValueAxis-1') { - await this.toggleAccordion(`yAxisAccordion${axisId}`); - await this.toggleAccordion(`yAxisOptionsAccordion${axisId}`); - - await testSubjects.click('yAxisSetYExtents'); - await testSubjects.setValue('yAxisYExtentsMax', max); - await testSubjects.setValue('yAxisYExtentsMin', min); - } - - async getField() { - return await comboBox.getComboBoxSelectedOptions('visDefaultEditorField'); - } - - async selectField(fieldValue, groupName = 'buckets', childAggregationType = null) { - log.debug(`selectField ${fieldValue}`); - const selector = ` - [group-name="${groupName}"] - [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen - [data-test-subj="visAggEditorParams"] - ${childAggregationType ? '.visEditorAgg__subAgg' : ''} - [data-test-subj="visDefaultEditorField"] - `; - const fieldEl = await find.byCssSelector(selector); - await comboBox.setElement(fieldEl, fieldValue); - } - - async selectAggregateWith(fieldValue) { - await testSubjects.selectValue('visDefaultEditorAggregateWith', fieldValue); - } - - async getInterval() { - return await comboBox.getComboBoxSelectedOptions('visEditorInterval'); - } - - async setInterval(newValue) { - log.debug(`Visualize.setInterval(${newValue})`); - return await comboBox.set('visEditorInterval', newValue); - } - - async setCustomInterval(newValue) { - log.debug(`Visualize.setCustomInterval(${newValue})`); - return await comboBox.setCustom('visEditorInterval', newValue); - } - - async getNumericInterval(agg = 2) { - return await testSubjects.getAttribute(`visEditorInterval${agg}`, 'value'); - } - - async setNumericInterval(newValue, { append } = {}, agg = 2) { - if (append) { - await testSubjects.append(`visEditorInterval${agg}`, String(newValue)); - } else { - await testSubjects.setValue(`visEditorInterval${agg}`, String(newValue)); - } - } - - async setSize(newValue, aggId) { - const dataTestSubj = aggId - ? `visEditorAggAccordion${aggId} > sizeParamEditor` - : 'sizeParamEditor'; - await testSubjects.setValue(dataTestSubj, String(newValue)); - } - - async toggleDisabledAgg(agg) { - await testSubjects.click(`visEditorAggAccordion${agg} > ~toggleDisableAggregationBtn`); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async toggleAggregationEditor(agg) { - await find.clickByCssSelector( - `[data-test-subj="visEditorAggAccordion${agg}"] .euiAccordion__button` - ); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async toggleOtherBucket(agg = 2) { - return await testSubjects.click(`visEditorAggAccordion${agg} > otherBucketSwitch`); - } - - async toggleMissingBucket(agg = 2) { - return await testSubjects.click(`visEditorAggAccordion${agg} > missingBucketSwitch`); - } - - async toggleScaleMetrics() { - return await testSubjects.click('scaleMetricsSwitch'); - } - - async isApplyEnabled() { - const applyButton = await testSubjects.find('visualizeEditorRenderButton'); - return await applyButton.isEnabled(); - } - - async clickGo() { - const prevRenderingCount = await this.getVisualizationRenderingCount(); - log.debug(`Before Rendering count ${prevRenderingCount}`); - await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton'); - await this.waitForRenderingCount(prevRenderingCount + 1); - } - - async clickReset() { - await testSubjects.click('visualizeEditorResetButton'); - await this.waitForVisualization(); - } - - async toggleAutoMode() { - await testSubjects.click('visualizeEditorAutoButton'); - } - - async sizeUpEditor() { - await testSubjects.click('visualizeEditorResizer'); - await browser.pressKeys(browser.keys.ARROW_RIGHT); - } - - async clickOptions() { - await find.clickByPartialLinkText('Options'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async changeHeatmapColorNumbers(value = 6) { - const input = await testSubjects.find(`heatmapColorsNumber`); - await input.clearValueWithKeyboard(); - await input.type(`${value}`); - } - - async clickMetricsAndAxes() { - await testSubjects.click('visEditorTabadvanced'); - } - - async clickOptionsTab() { - await testSubjects.click('visEditorTaboptions'); - } - - async clickEnableCustomRanges() { - await testSubjects.click('heatmapUseCustomRanges'); - } - - async clickAddRange() { - await testSubjects.click(`heatmapColorRange__addRangeButton`); - } - - async setCustomRangeByIndex(index, from, to) { - await testSubjects.setValue(`heatmapColorRange${index}__from`, from); - await testSubjects.setValue(`heatmapColorRange${index}__to`, to); - } - - async clickYAxisOptions(axisId) { - await testSubjects.click(`toggleYAxisOptions-${axisId}`); - } - - async clickYAxisAdvancedOptions(axisId) { - await testSubjects.click(`toggleYAxisAdvancedOptions-${axisId}`); - } - - async changeYAxisFilterLabelsCheckbox(axisId, enabled) { - const selector = `yAxisFilterLabelsCheckbox-${axisId}`; - enabled ? await this.checkCheckbox(selector) : await this.uncheckCheckbox(selector); - } - - async selectChartMode(mode) { - const selector = await find.byCssSelector(`#seriesMode0 > option[value="${mode}"]`); - await selector.click(); - } - - async selectYAxisScaleType(axisId, scaleType) { - const selectElement = await testSubjects.find(`scaleSelectYAxis-${axisId}`); - const selector = await selectElement.findByCssSelector(`option[value="${scaleType}"]`); - await selector.click(); - } - - async selectYAxisMode(mode) { - const selector = await find.byCssSelector(`#valueAxisMode0 > option[value="${mode}"]`); - await selector.click(); - } - - async clickData() { - await testSubjects.click('visualizeEditDataLink'); - } - - async clickVisEditorTab(tabName) { - await testSubjects.click('visEditorTab' + tabName); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async selectWMS() { - await find.clickByCssSelector('input[name="wms.enabled"]'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async ensureSavePanelOpen() { - log.debug('ensureSavePanelOpen'); - await PageObjects.header.waitUntilLoadingHasFinished(); - const isOpen = await testSubjects.exists('savedObjectSaveModal', { timeout: 5000 }); - if (!isOpen) { - await testSubjects.click('visualizeSaveButton'); - } - } - - async saveVisualization(vizName, { saveAsNew = false } = {}) { - await this.ensureSavePanelOpen(); - await testSubjects.setValue('savedObjectTitle', vizName); - if (saveAsNew) { - log.debug('Check save as new visualization'); - await testSubjects.click('saveAsNewCheckbox'); - } - log.debug('Click Save Visualization button'); - - await testSubjects.click('confirmSaveSavedObjectButton'); - - // Confirm that the Visualization has actually been saved - await testSubjects.existOrFail('saveVisualizationSuccess'); - const message = await PageObjects.common.closeToast(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.common.waitForSaveModalToClose(); - - return message; - } - - async saveVisualizationExpectSuccess(vizName, { saveAsNew = false } = {}) { - const saveMessage = await this.saveVisualization(vizName, { saveAsNew }); - if (!saveMessage) { - throw new Error( - `Expected saveVisualization to respond with the saveMessage from the toast, got ${saveMessage}` - ); - } - } - - async saveVisualizationExpectSuccessAndBreadcrumb(vizName, { saveAsNew = false } = {}) { - await this.saveVisualizationExpectSuccess(vizName, { saveAsNew }); - await retry.waitFor( - 'last breadcrumb to have new vis name', - async () => (await globalNav.getLastBreadcrumb()) === vizName - ); - } - - async clickLoadSavedVisButton() { - // TODO: Use a test subject selector once we rewrite breadcrumbs to accept each breadcrumb - // element as a child instead of building the breadcrumbs dynamically. - await find.clickByCssSelector('[href="#/visualize"]'); - } - - async filterVisByName(vizName) { - const input = await find.byCssSelector('input[name="filter"]'); - await input.click(); - // can't uses dashes in saved visualizations when filtering - // or extended character sets - // https://github.com/elastic/kibana/issues/6300 - await input.type(vizName.replace('-', ' ')); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async clickVisualizationByName(vizName) { - log.debug('clickVisualizationByLinkText(' + vizName + ')'); - return find.clickByPartialLinkText(vizName); - } - - async loadSavedVisualization(vizName, { navigateToVisualize = true } = {}) { - if (navigateToVisualize) { - await this.clickLoadSavedVisButton(); - } - await this.openSavedVisualization(vizName); - } - - async openSavedVisualization(vizName) { - await this.clickVisualizationByName(vizName); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async getXAxisLabels() { - const xAxis = await find.byCssSelector('.visAxis--x.visAxis__column--bottom'); - const $ = await xAxis.parseDomContent(); - return $('.x > g > text') - .toArray() - .map(tick => - $(tick) - .text() - .trim() - ); - } - - async getYAxisLabels() { - const yAxis = await find.byCssSelector('.visAxis__column--y.visAxis__column--left'); - const $ = await yAxis.parseDomContent(); - return $('.y > g > text') - .toArray() - .map(tick => - $(tick) - .text() - .trim() - ); - } - - /** - * Removes chrome and takes a small screenshot of a vis to compare against a baseline. - * @param {string} name The name of the baseline image. - * @param {object} opts Options object. - * @param {number} opts.threshold Threshold for allowed variance when comparing images. - */ - async expectVisToMatchScreenshot(name, opts = { threshold: 0.05 }) { - log.debug(`expectVisToMatchScreenshot(${name})`); - - // Collapse sidebar and inject some CSS to hide the nav so we have a focused screenshot - await this.clickEditorSidebarCollapse(); - await this.waitForVisualizationRenderingStabilized(); - await browser.execute(` - var el = document.createElement('style'); - el.id = '__data-test-style'; - el.innerHTML = '[data-test-subj="headerGlobalNav"] { display: none; } '; - el.innerHTML += '[data-test-subj="top-nav"] { display: none; } '; - el.innerHTML += '[data-test-subj="experimentalVisInfo"] { display: none; } '; - document.body.appendChild(el); - `); - - const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines); - - // Reset the chart to its original state - await browser.execute(` - var el = document.getElementById('__data-test-style'); - document.body.removeChild(el); - `); - await this.clickEditorSidebarCollapse(); - await this.waitForVisualizationRenderingStabilized(); - expect(percentDifference).to.be.lessThan(opts.threshold); - } - - /* - ** This method gets the chart data and scales it based on chart height and label. - ** Returns an array of height values - */ - async getAreaChartData(dataLabel, axis = 'ValueAxis-1') { - const yAxisRatio = await this.getChartYAxisRatio(axis); - - const rectangle = await find.byCssSelector('rect.background'); - const yAxisHeight = await rectangle.getAttribute('height'); - log.debug(`height --------- ${yAxisHeight}`); - - const path = await retry.try( - async () => - await find.byCssSelector(`path[data-label="${dataLabel}"]`, defaultFindTimeout * 2) - ); - const data = await path.getAttribute('d'); - log.debug(data); - // This area chart data starts with a 'M'ove to a x,y location, followed - // by a bunch of 'L'ines from that point to the next. Those points are - // the values we're going to use to calculate the data values we're testing. - // So git rid of the one 'M' and split the rest on the 'L's. - const tempArray = data - .replace('M ', '') - .replace('M', '') - .replace(/ L /g, 'L') - .replace(/ /g, ',') - .split('L'); - const chartSections = tempArray.length / 2; - // log.debug('chartSections = ' + chartSections + ' height = ' + yAxisHeight + ' yAxisLabel = ' + yAxisLabel); - const chartData = []; - for (let i = 0; i < chartSections; i++) { - chartData[i] = Math.round((yAxisHeight - tempArray[i].split(',')[1]) * yAxisRatio); - log.debug('chartData[i] =' + chartData[i]); - } - return chartData; - } - - /* - ** This method returns the paths that compose an area chart. - */ - async getAreaChartPaths(dataLabel) { - const path = await retry.try( - async () => - await find.byCssSelector(`path[data-label="${dataLabel}"]`, defaultFindTimeout * 2) - ); - const data = await path.getAttribute('d'); - log.debug(data); - // This area chart data starts with a 'M'ove to a x,y location, followed - // by a bunch of 'L'ines from that point to the next. Those points are - // the values we're going to use to calculate the data values we're testing. - // So git rid of the one 'M' and split the rest on the 'L's. - return data.split('L'); - } - - // The current test shows dots, not a line. This function gets the dots and normalizes their height. - async getLineChartData(dataLabel = 'Count', axis = 'ValueAxis-1') { - // 1). get the range/pixel ratio - const yAxisRatio = await this.getChartYAxisRatio(axis); - // 2). find and save the y-axis pixel size (the chart height) - const rectangle = await find.byCssSelector('clipPath rect'); - const yAxisHeight = await rectangle.getAttribute('height'); - // 3). get the visWrapper__chart elements - const chartTypes = await retry.try( - async () => - await find.allByCssSelector( - `.visWrapper__chart circle[data-label="${dataLabel}"][fill-opacity="1"]`, - defaultFindTimeout * 2 - ) - ); - // 4). for each chart element, find the green circle, then the cy position - const chartData = await Promise.all( - chartTypes.map(async chart => { - const cy = await chart.getAttribute('cy'); - // the point_series_options test has data in the billions range and - // getting 11 digits of precision with these calculations is very hard - return Math.round(((yAxisHeight - cy) * yAxisRatio).toPrecision(6)); - }) - ); - - return chartData; - } - - // this is ALMOST identical to DiscoverPage.getBarChartData - async getBarChartData(dataLabel = 'Count', axis = 'ValueAxis-1') { - // 1). get the range/pixel ratio - const yAxisRatio = await this.getChartYAxisRatio(axis); - // 3). get the visWrapper__chart elements - const svg = await find.byCssSelector('div.chart'); - const $ = await svg.parseDomContent(); - const chartData = $(`g > g.series > rect[data-label="${dataLabel}"]`) - .toArray() - .map(chart => { - const barHeight = $(chart).attr('height'); - return Math.round(barHeight * yAxisRatio); - }); - - return chartData; - } - - // Returns value per pixel - async getChartYAxisRatio(axis = 'ValueAxis-1') { - // 1). get the maximum chart Y-Axis marker value and Y position - const maxYAxisChartMarker = await retry.try( - async () => - await find.byCssSelector( - `div.visAxis__splitAxes--y > div > svg > g.${axis} > g:last-of-type.tick` - ) - ); - const maxYLabel = (await maxYAxisChartMarker.getVisibleText()).replace(/,/g, ''); - const maxYLabelYPosition = (await maxYAxisChartMarker.getPosition()).y; - log.debug(`maxYLabel = ${maxYLabel}, maxYLabelYPosition = ${maxYLabelYPosition}`); - - // 2). get the minimum chart Y-Axis marker value and Y position - const minYAxisChartMarker = await find.byCssSelector( - 'div.visAxis__column--y.visAxis__column--left > div > div > svg:nth-child(2) > g > g:nth-child(1).tick' - ); - const minYLabel = (await minYAxisChartMarker.getVisibleText()).replace(',', ''); - const minYLabelYPosition = (await minYAxisChartMarker.getPosition()).y; - return (maxYLabel - minYLabel) / (minYLabelYPosition - maxYLabelYPosition); - } - - async getHeatmapData() { - const chartTypes = await retry.try( - async () => await find.allByCssSelector('svg > g > g.series rect', defaultFindTimeout * 2) - ); - log.debug('rects=' + chartTypes); - async function getChartType(chart) { - return await chart.getAttribute('data-label'); - } - const getChartTypesPromises = chartTypes.map(getChartType); - return await Promise.all(getChartTypesPromises); - } - - async expectError() { - return await testSubjects.existOrFail('visLibVisualizeError'); - } - - async getChartAreaWidth() { - const rect = await retry.try(async () => find.byCssSelector('clipPath rect')); - return await rect.getAttribute('width'); - } - - async getChartAreaHeight() { - const rect = await retry.try(async () => find.byCssSelector('clipPath rect')); - return await rect.getAttribute('height'); - } - - /** - * If you are writing new tests, you should rather look into getTableVisContent method instead. - */ - async getTableVisData() { - return await testSubjects.getVisibleText('paginated-table-body'); - } - - /** - * This function is the newer function to retrieve data from within a table visualization. - * It uses a better return format, than the old getTableVisData, by properly splitting - * cell values into arrays. Please use this function for newer tests. - */ - async getTableVisContent({ stripEmptyRows = true } = {}) { - return await retry.try(async () => { - const container = await testSubjects.find('tableVis'); - const allTables = await testSubjects.findAllDescendant('paginated-table-body', container); - - if (allTables.length === 0) { - return []; - } - - const allData = await Promise.all( - allTables.map(async t => { - let data = await table.getDataFromElement(t); - if (stripEmptyRows) { - data = data.filter(row => row.length > 0 && row.some(cell => cell.trim().length > 0)); - } - return data; - }) - ); - - if (allTables.length === 1) { - // If there was only one table we return only the data for that table - // This prevents an unnecessary array around that single table, which - // is the case we have in most tests. - return allData[0]; - } - - return allData; - }); - } - - async toggleIsFilteredByCollarCheckbox() { - await testSubjects.click('isFilteredByCollarCheckbox'); - } - - async setIsFilteredByCollarCheckbox(value = true) { - await retry.try(async () => { - const isChecked = await this.isSwitchChecked('isFilteredByCollarCheckbox'); - if (isChecked !== value) { - await testSubjects.click('isFilteredByCollarCheckbox'); - throw new Error('isFilteredByCollar not set correctly'); - } - }); - } - - async getMarkdownData() { - const markdown = await retry.try(async () => find.byCssSelector('visualize')); - return await markdown.getVisibleText(); - } - - async getVisualizationRenderingCount() { - const visualizationLoader = await testSubjects.find('visualizationLoader'); - const renderingCount = await visualizationLoader.getAttribute('data-rendering-count'); - return Number(renderingCount); - } - - async waitForRenderingCount(minimumCount = 1) { - await retry.waitFor( - `rendering count to be greater than or equal to [${minimumCount}]`, - async () => { - const currentRenderingCount = await this.getVisualizationRenderingCount(); - log.debug(`-- currentRenderingCount=${currentRenderingCount}`); - return currentRenderingCount >= minimumCount; - } - ); - } - - async waitForVisualizationRenderingStabilized() { - //assuming rendering is done when data-rendering-count is constant within 1000 ms - await retry.waitFor('rendering count to stabilize', async () => { - const firstCount = await this.getVisualizationRenderingCount(); - log.debug(`-- firstCount=${firstCount}`); - - await PageObjects.common.sleep(1000); - - const secondCount = await this.getVisualizationRenderingCount(); - log.debug(`-- secondCount=${secondCount}`); - - return firstCount === secondCount; - }); - } - - async waitForVisualization() { - await this.waitForVisualizationRenderingStabilized(); - return await find.byCssSelector('.visualization'); - } - - async waitForVisualizationSavedToastGone() { - return await testSubjects.waitForDeleted('saveVisualizationSuccess'); - } - - async getZoomSelectors(zoomSelector) { - return await find.allByCssSelector(zoomSelector); - } - - async clickMapButton(zoomSelector, waitForLoading) { - await retry.try(async () => { - const zooms = await this.getZoomSelectors(zoomSelector); - await Promise.all(zooms.map(async zoom => await zoom.click())); - if (waitForLoading) { - await PageObjects.header.waitUntilLoadingHasFinished(); - } - }); - } - - async getVisualizationRequest() { - log.debug('getVisualizationRequest'); - await inspector.open(); - await testSubjects.click('inspectorViewChooser'); - await testSubjects.click('inspectorViewChooserRequests'); - await testSubjects.click('inspectorRequestDetailRequest'); - return await testSubjects.getVisibleText('inspectorRequestBody'); - } - - async getVisualizationResponse() { - log.debug('getVisualizationResponse'); - await inspector.open(); - await testSubjects.click('inspectorViewChooser'); - await testSubjects.click('inspectorViewChooserRequests'); - await testSubjects.click('inspectorRequestDetailResponse'); - return await testSubjects.getVisibleText('inspectorResponseBody'); - } - - async getMapBounds() { - const request = await this.getVisualizationRequest(); - const requestObject = JSON.parse(request); - return requestObject.aggs.filter_agg.filter.geo_bounding_box['geo.coordinates']; - } - - async clickMapZoomIn(waitForLoading = true) { - await this.clickMapButton('a.leaflet-control-zoom-in', waitForLoading); - } - - async clickMapZoomOut(waitForLoading = true) { - await this.clickMapButton('a.leaflet-control-zoom-out', waitForLoading); - } - - async getMapZoomEnabled(zoomSelector) { - const zooms = await this.getZoomSelectors(zoomSelector); - const classAttributes = await Promise.all( - zooms.map(async zoom => await zoom.getAttribute('class')) - ); - return !classAttributes.join('').includes('leaflet-disabled'); - } - - async zoomAllTheWayOut() { - // we can tell we're at level 1 because zoom out is disabled - return await retry.try(async () => { - await this.clickMapZoomOut(); - const enabled = await this.getMapZoomOutEnabled(); - //should be able to zoom more as current config has 0 as min level. - if (enabled) { - throw new Error('Not fully zoomed out yet'); - } - }); - } - - async getMapZoomInEnabled() { - return await this.getMapZoomEnabled('a.leaflet-control-zoom-in'); - } - - async getMapZoomOutEnabled() { - return await this.getMapZoomEnabled('a.leaflet-control-zoom-out'); - } - - async clickMapFitDataBounds() { - return await this.clickMapButton('a.fa-crop'); - } - - async clickLandingPageBreadcrumbLink() { - log.debug('clickLandingPageBreadcrumbLink'); - await find.clickByCssSelector(`a[href="#${VisualizeConstants.LANDING_PAGE_PATH}"]`); - } - - /** - * Returns true if already on the landing page (that page doesn't have a link to itself). - * @returns {Promise} - */ - async onLandingPage() { - log.debug(`VisualizePage.onLandingPage`); - const exists = await testSubjects.exists('visualizeLandingPage'); - return exists; - } - - async gotoLandingPage() { - log.debug('VisualizePage.gotoLandingPage'); - const onPage = await this.onLandingPage(); - if (!onPage) { - await retry.try(async () => { - await this.clickLandingPageBreadcrumbLink(); - const onLandingPage = await this.onLandingPage(); - if (!onLandingPage) throw new Error('Not on the landing page.'); - }); - } - } - - async getLegendEntries() { - const legendEntries = await find.allByCssSelector( - '.visLegend__button', - defaultFindTimeout * 2 - ); - return await Promise.all( - legendEntries.map(async chart => await chart.getAttribute('data-label')) - ); - } - - async openLegendOptionColors(name) { - await this.waitForVisualizationRenderingStabilized(); - await retry.try(async () => { - // This click has been flaky in opening the legend, hence the retry. See - // https://github.com/elastic/kibana/issues/17468 - await testSubjects.click(`legend-${name}`); - await this.waitForVisualizationRenderingStabilized(); - // arbitrary color chosen, any available would do - const isOpen = await this.doesLegendColorChoiceExist('#EF843C'); - if (!isOpen) { - throw new Error('legend color selector not open'); - } - }); - } - - async filterOnTableCell(column, row) { - await retry.try(async () => { - const table = await testSubjects.find('tableVis'); - const cell = await table.findByCssSelector( - `tbody tr:nth-child(${row}) td:nth-child(${column})` - ); - await cell.moveMouseTo(); - const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell); - await filterBtn.click(); - }); - } - - async toggleLegend(show = true) { - await retry.try(async () => { - const isVisible = find.byCssSelector('.visLegend'); - if ((show && !isVisible) || (!show && isVisible)) { - await testSubjects.click('vislibToggleLegend'); - } - }); - } - - async filterLegend(name) { - await this.toggleLegend(); - await testSubjects.click(`legend-${name}`); - const filters = await testSubjects.find(`legend-${name}-filters`); - const [filterIn] = await filters.findAllByCssSelector(`input`); - await filterIn.click(); - await this.waitForVisualizationRenderingStabilized(); - } - - async doesLegendColorChoiceExist(color) { - return await testSubjects.exists(`legendSelectColor-${color}`); - } - - async selectNewLegendColorChoice(color) { - await testSubjects.click(`legendSelectColor-${color}`); - } - - async doesSelectedLegendColorExist(color) { - return await testSubjects.exists(`legendSelectedColor-${color}`); - } - - async getYAxisTitle() { - const title = await find.byCssSelector('.y-axis-div .y-axis-title text'); - return await title.getVisibleText(); - } - - async selectBucketType(type) { - const bucketType = await find.byCssSelector(`[data-test-subj="${type}"]`); - return await bucketType.click(); - } - - async getBucketErrorMessage() { - const error = await find.byCssSelector( - '[group-name="buckets"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText' - ); - const errorMessage = await error.getAttribute('innerText'); - log.debug(errorMessage); - return errorMessage; - } - - async selectOrderByMetric(agg, metric) { - const sortSelect = await testSubjects.find(`visEditorOrderBy${agg}`); - const sortMetric = await sortSelect.findByCssSelector(`option[value="${metric}"]`); - await sortMetric.click(); - } - - async selectCustomSortMetric(agg, metric, field) { - await this.selectOrderByMetric(agg, 'custom'); - await this.selectAggregation(metric, 'buckets', true); - await this.selectField(field, 'buckets', true); - } - - async clickSplitDirection(direction) { - const control = await testSubjects.find('visEditorSplitBy'); - const radioBtn = await control.findByCssSelector(`[title="${direction}"]`); - await radioBtn.click(); - } - - async countNestedTables() { - const vis = await testSubjects.find('tableVis'); - const result = []; - - for (let i = 1; true; i++) { - const selector = new Array(i).fill('.kbnAggTable__group').join(' '); - const tables = await vis.findAllByCssSelector(selector); - if (tables.length === 0) { - break; - } - result.push(tables.length); - } - - return result; - } - - async removeDimension(agg) { - await testSubjects.click(`visEditorAggAccordion${agg} > removeDimensionBtn`); - } - - async setFilterParams({ aggNth = 0, indexPattern, field }) { - await comboBox.set(`indexPatternSelect-${aggNth}`, indexPattern); - await comboBox.set(`fieldSelect-${aggNth}`, field); - } - - async setFilterRange({ aggNth = 0, min, max }) { - const control = await testSubjects.find(`inputControl${aggNth}`); - const inputMin = await control.findByCssSelector('[name$="minValue"]'); - await inputMin.type(min); - const inputMax = await control.findByCssSelector('[name$="maxValue"]'); - await inputMax.type(max); - } - - async scrollSubjectIntoView(subject) { - const element = await testSubjects.find(subject); - await element.scrollIntoViewIfNecessary(); - } - } - - return new VisualizePage(); -} diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts new file mode 100644 index 0000000000000..1562cf9745f2d --- /dev/null +++ b/test/functional/page_objects/visualize_page.ts @@ -0,0 +1,328 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; +import { VisualizeConstants } from '../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; + +export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const find = getService('find'); + const log = getService('log'); + const globalNav = getService('globalNav'); + const listingTable = getService('listingTable'); + const { common, header, visEditor } = getPageObjects(['common', 'header', 'visEditor']); + + /** + * This page object contains the visualization type selection, the landing page, + * and the open/save dialog functions + */ + class VisualizePage { + index = { + LOGSTASH_TIME_BASED: 'logstash-*', + LOGSTASH_NON_TIME_BASED: 'logstash*', + }; + + public async gotoVisualizationLandingPage() { + await common.navigateToApp('visualize'); + } + + public async clickNewVisualization() { + // newItemButton button is only visible when there are items in the listing table is displayed. + let exists = await testSubjects.exists('newItemButton'); + if (exists) { + return await testSubjects.click('newItemButton'); + } + + exists = await testSubjects.exists('createVisualizationPromptButton'); + // no viz exist, click createVisualizationPromptButton to create new dashboard + return await this.createVisualizationPromptButton(); + } + + public async createVisualizationPromptButton() { + await testSubjects.click('createVisualizationPromptButton'); + } + + public async getChartTypes() { + const chartTypeField = await testSubjects.find('visNewDialogTypes'); + const $ = await chartTypeField.parseDomContent(); + return $('button') + .toArray() + .map(chart => + $(chart) + .findTestSubject('visTypeTitle') + .text() + .trim() + ); + } + + public async waitForVisualizationSelectPage() { + await retry.try(async () => { + const visualizeSelectTypePage = await testSubjects.find('visNewDialogTypes'); + if (!(await visualizeSelectTypePage.isDisplayed())) { + throw new Error('wait for visualization select page'); + } + }); + } + + public async navigateToNewVisualization() { + await common.navigateToApp('visualize'); + await this.clickNewVisualization(); + await this.waitForVisualizationSelectPage(); + } + + public async clickVisType(type: string) { + await testSubjects.click(`visType-${type}`); + await header.waitUntilLoadingHasFinished(); + } + + public async clickAreaChart() { + await this.clickVisType('area'); + } + + public async clickDataTable() { + await this.clickVisType('table'); + } + + public async clickLineChart() { + await this.clickVisType('line'); + } + + public async clickRegionMap() { + await this.clickVisType('region_map'); + } + + public async clickMarkdownWidget() { + await this.clickVisType('markdown'); + } + + public async clickMetric() { + await this.clickVisType('metric'); + } + + public async clickGauge() { + await this.clickVisType('gauge'); + } + + public async clickPieChart() { + await this.clickVisType('pie'); + } + + public async clickTileMap() { + await this.clickVisType('tile_map'); + } + + public async clickTagCloud() { + await this.clickVisType('tagcloud'); + } + + public async clickVega() { + await this.clickVisType('vega'); + } + + public async clickVisualBuilder() { + await this.clickVisType('metrics'); + } + + public async clickVerticalBarChart() { + await this.clickVisType('histogram'); + } + + public async clickHeatmapChart() { + await this.clickVisType('heatmap'); + } + + public async clickInputControlVis() { + await this.clickVisType('input_control_vis'); + } + + public async createSimpleMarkdownViz(vizName: string) { + await this.gotoVisualizationLandingPage(); + await this.navigateToNewVisualization(); + await this.clickMarkdownWidget(); + await visEditor.setMarkdownTxt(vizName); + await visEditor.clickGo(); + await this.saveVisualization(vizName); + } + + public async clickNewSearch(indexPattern = this.index.LOGSTASH_TIME_BASED) { + await testSubjects.click(`savedObjectTitle${indexPattern.split(' ').join('-')}`); + await header.waitUntilLoadingHasFinished(); + } + + public async selectVisSourceIfRequired() { + log.debug('selectVisSourceIfRequired'); + const selectPage = await testSubjects.findAll('visualizeSelectSearch'); + if (selectPage.length) { + log.debug('a search is required for this visualization'); + await this.clickNewSearch(); + } + } + + /** + * Deletes all existing visualizations + */ + public async deleteAllVisualizations() { + await retry.try(async () => { + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await common.clickConfirmOnModal(); + await testSubjects.find('createVisualizationPromptButton'); + }); + } + + public async isBetaInfoShown() { + return await testSubjects.exists('betaVisInfo'); + } + + public async getBetaTypeLinks() { + return await find.allByCssSelector('[data-vis-stage="beta"]'); + } + + public async getExperimentalTypeLinks() { + return await find.allByCssSelector('[data-vis-stage="experimental"]'); + } + + public async isExperimentalInfoShown() { + return await testSubjects.exists('experimentalVisInfo'); + } + + public async getExperimentalInfo() { + return await testSubjects.find('experimentalVisInfo'); + } + + public async getSideEditorExists() { + return await find.existsByCssSelector('.collapsible-sidebar'); + } + + public async clickSavedSearch(savedSearchName: string) { + await testSubjects.click(`savedObjectTitle${savedSearchName.split(' ').join('-')}`); + await header.waitUntilLoadingHasFinished(); + } + + public async clickUnlinkSavedSearch() { + await testSubjects.doubleClick('unlinkSavedSearch'); + await header.waitUntilLoadingHasFinished(); + } + + public async ensureSavePanelOpen() { + log.debug('ensureSavePanelOpen'); + await header.waitUntilLoadingHasFinished(); + const isOpen = await testSubjects.exists('savedObjectSaveModal', { timeout: 5000 }); + if (!isOpen) { + await testSubjects.click('visualizeSaveButton'); + } + } + + public async clickLoadSavedVisButton() { + // TODO: Use a test subject selector once we rewrite breadcrumbs to accept each breadcrumb + // element as a child instead of building the breadcrumbs dynamically. + await find.clickByCssSelector('[href="#/visualize"]'); + } + + public async clickVisualizationByName(vizName: string) { + log.debug('clickVisualizationByLinkText(' + vizName + ')'); + await find.clickByPartialLinkText(vizName); + } + + public async loadSavedVisualization(vizName: string, { navigateToVisualize = true } = {}) { + if (navigateToVisualize) { + await this.clickLoadSavedVisButton(); + } + await this.openSavedVisualization(vizName); + } + + public async openSavedVisualization(vizName: string) { + await this.clickVisualizationByName(vizName); + await header.waitUntilLoadingHasFinished(); + } + + public async waitForVisualizationSavedToastGone() { + await testSubjects.waitForDeleted('saveVisualizationSuccess'); + } + + public async clickLandingPageBreadcrumbLink() { + log.debug('clickLandingPageBreadcrumbLink'); + await find.clickByCssSelector(`a[href="#${VisualizeConstants.LANDING_PAGE_PATH}"]`); + } + + /** + * Returns true if already on the landing page (that page doesn't have a link to itself). + * @returns {Promise} + */ + public async onLandingPage() { + log.debug(`VisualizePage.onLandingPage`); + return await testSubjects.exists('visualizeLandingPage'); + } + + public async gotoLandingPage() { + log.debug('VisualizePage.gotoLandingPage'); + const onPage = await this.onLandingPage(); + if (!onPage) { + await retry.try(async () => { + await this.clickLandingPageBreadcrumbLink(); + const onLandingPage = await this.onLandingPage(); + if (!onLandingPage) throw new Error('Not on the landing page.'); + }); + } + } + + public async saveVisualization(vizName: string, { saveAsNew = false } = {}) { + await this.ensureSavePanelOpen(); + await testSubjects.setValue('savedObjectTitle', vizName); + if (saveAsNew) { + log.debug('Check save as new visualization'); + await testSubjects.click('saveAsNewCheckbox'); + } + log.debug('Click Save Visualization button'); + + await testSubjects.click('confirmSaveSavedObjectButton'); + + // Confirm that the Visualization has actually been saved + await testSubjects.existOrFail('saveVisualizationSuccess'); + const message = await common.closeToast(); + await header.waitUntilLoadingHasFinished(); + await common.waitForSaveModalToClose(); + + return message; + } + + public async saveVisualizationExpectSuccess(vizName: string, { saveAsNew = false } = {}) { + const saveMessage = await this.saveVisualization(vizName, { saveAsNew }); + if (!saveMessage) { + throw new Error( + `Expected saveVisualization to respond with the saveMessage from the toast, got ${saveMessage}` + ); + } + } + + public async saveVisualizationExpectSuccessAndBreadcrumb( + vizName: string, + { saveAsNew = false } = {} + ) { + await this.saveVisualizationExpectSuccess(vizName, { saveAsNew }); + await retry.waitFor( + 'last breadcrumb to have new vis name', + async () => (await globalNav.getLastBreadcrumb()) === vizName + ); + } + } + + return new VisualizePage(); +} diff --git a/test/functional/services/apps_menu.ts b/test/functional/services/apps_menu.ts index a4cd98b2a06ec..fe17532f6a41a 100644 --- a/test/functional/services/apps_menu.ts +++ b/test/functional/services/apps_menu.ts @@ -25,7 +25,7 @@ export function AppsMenuProvider({ getService }: FtrProviderContext) { return new (class AppsMenu { /** - * Get the text and href from each of the links in the apps menu + * Get the attributes from each of the links in the apps menu */ public async readLinks() { const appMenu = await testSubjects.find('navDrawer'); @@ -37,12 +37,21 @@ export function AppsMenuProvider({ getService }: FtrProviderContext) { return { text: $(link).text(), href: $(link).attr('href'), + disabled: $(link).attr('disabled') != null, }; }); return links; } + /** + * Get the attributes from the link with the given name. + * @param name + */ + public async getLink(name: string) { + return (await this.readLinks()).find(nl => nl.text === name); + } + /** * Determine if an app link with the given name exists * @param name diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js index c4e7fe6ad3bd9..5e722ccce8970 100644 --- a/test/functional/services/dashboard/visualizations.js +++ b/test/functional/services/dashboard/visualizations.js @@ -24,7 +24,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); - const PageObjects = getPageObjects(['dashboard', 'visualize', 'header', 'discover']); + const PageObjects = getPageObjects(['dashboard', 'visualize', 'visEditor', 'header', 'discover']); return new (class DashboardVisualizations { async createAndAddTSVBVisualization(name) { @@ -107,8 +107,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { } await this.ensureNewVisualizationDialogIsShowing(); await PageObjects.visualize.clickMarkdownWidget(); - await PageObjects.visualize.setMarkdownTxt(markdown); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.setMarkdownTxt(markdown); + await PageObjects.visEditor.clickGo(); await PageObjects.visualize.saveVisualizationExpectSuccess(name); } })(); diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index ea47ccf1d2704..a10bb013b3af4 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -49,7 +49,7 @@ import { TestSubjectsProvider } from './test_subjects'; import { ToastsProvider } from './toasts'; // @ts-ignore not TS yet import { PieChartProvider } from './visualizations'; -import { VisualizeListingTableProvider } from './visualize_listing_table'; +import { ListingTableProvider } from './listing_table'; import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; export const services = { @@ -66,7 +66,7 @@ export const services = { dashboardVisualizations: DashboardVisualizationProvider, dashboardExpect: DashboardExpectProvider, failureDebugging: FailureDebuggingProvider, - visualizeListingTable: VisualizeListingTableProvider, + listingTable: ListingTableProvider, dashboardAddPanel: DashboardAddPanelProvider, dashboardReplacePanel: DashboardReplacePanelProvider, dashboardPanelActions: DashboardPanelActionsProvider, diff --git a/test/functional/services/visualize_listing_table.ts b/test/functional/services/listing_table.ts similarity index 50% rename from test/functional/services/visualize_listing_table.ts rename to test/functional/services/listing_table.ts index 8c4640ada1c05..ec886cf694f2e 100644 --- a/test/functional/services/visualize_listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -19,13 +19,25 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function VisualizeListingTableProvider({ getService, getPageObjects }: FtrProviderContext) { +export function ListingTableProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const log = getService('log'); - const { header } = getPageObjects(['header']); + const retry = getService('retry'); + const { common, header } = getPageObjects(['common', 'header']); + + class ListingTable { + public async getSearchFilter() { + const searchFilter = await find.allByCssSelector('.euiFieldSearch'); + return searchFilter[0]; + } + + public async clearFilter() { + const searchFilter = await this.getSearchFilter(); + await searchFilter.clearValue(); + await searchFilter.click(); + } - class VisualizeListingTable { public async getAllVisualizationNamesOnCurrentPage(): Promise { const visualizationNames = []; const links = await find.allByCssSelector('.kuiLink'); @@ -36,8 +48,44 @@ export function VisualizeListingTableProvider({ getService, getPageObjects }: Ft return visualizationNames; } + public async getItemsCount(appName: 'visualize' | 'dashboard'): Promise { + const prefixMap = { visualize: 'vis', dashboard: 'dashboard' }; + const elements = await find.allByCssSelector( + `[data-test-subj^="${prefixMap[appName]}ListingTitleLink"]` + ); + return elements.length; + } + + public async searchForItemWithName(name: string) { + log.debug(`searchForItemWithName: ${name}`); + + await retry.try(async () => { + const searchFilter = await this.getSearchFilter(); + await searchFilter.clearValue(); + await searchFilter.click(); + // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. + await searchFilter.type(name.replace('-', ' ')); + await common.pressEnterKey(); + }); + + await header.waitUntilLoadingHasFinished(); + } + + public async clickDeleteSelected() { + await testSubjects.click('deleteSelectedItems'); + } + + public async checkListingSelectAllCheckbox() { + const element = await testSubjects.find('checkboxSelectAll'); + const isSelected = await element.isSelected(); + if (!isSelected) { + log.debug(`checking checkbox "checkboxSelectAll"`); + await testSubjects.click('checkboxSelectAll'); + } + } + public async getAllVisualizationNames(): Promise { - log.debug('VisualizeListingTable.getAllVisualizationNames'); + log.debug('ListingTable.getAllVisualizationNames'); let morePages = true; let visualizationNames: string[] = []; while (morePages) { @@ -54,5 +102,5 @@ export function VisualizeListingTableProvider({ getService, getPageObjects }: Ft } } - return new VisualizeListingTable(); + return new ListingTable(); } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index 69c2793621095..afe8499a1c2ea 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -100,7 +100,9 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { .subscribe({ next({ message, level }) { const msg = message.replace(/\\n/g, '\n'); - log[level === 'SEVERE' ? 'error' : 'debug'](`browser[${level}] ${msg}`); + log[level === 'SEVERE' || level === 'error' ? 'error' : 'debug']( + `browser[${level}] ${msg}` + ); }, }); diff --git a/test/functional/services/screenshots.ts b/test/functional/services/screenshots.ts index 9e673fe919a74..4c5728174cf99 100644 --- a/test/functional/services/screenshots.ts +++ b/test/functional/services/screenshots.ts @@ -51,7 +51,7 @@ export async function ScreenshotsProvider({ getService }: FtrProviderContext) { * @param updateBaselines {boolean} optional, pass true to update the baseline snapshot. * @return {Promise.} Percentage difference between the baseline and the current snapshot. */ - async compareAgainstBaseline(name: string, updateBaselines: boolean, el: WebElementWrapper) { + async compareAgainstBaseline(name: string, updateBaselines: boolean, el?: WebElementWrapper) { log.debug('compareAgainstBaseline'); const sessionPath = resolve(SESSION_DIRECTORY, `${name}.png`); await this._take(sessionPath, el); diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index a3f64e6f96cc8..8ef008d5dee50 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -295,6 +295,25 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { public getCssSelector(selector: string): string { return testSubjSelector(selector); } + + public async scrollIntoView(selector: string) { + const element = await this.find(selector); + await element.scrollIntoViewIfNecessary(); + } + + public async isChecked(selector: string) { + const checkbox = await this.find(selector); + return await checkbox.isSelected(); + } + + public async setCheckbox(selector: string, state: 'check' | 'uncheck') { + const isChecked = await this.isChecked(selector); + const states = { check: true, uncheck: false }; + if (isChecked !== states[state]) { + log.debug(`updating checkbox ${selector}`); + await this.click(selector); + } + } } return new TestSubjects(); diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 87026ce25d9aa..e9a4f3bcc4b1a 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -37,6 +37,7 @@ export default async function({ readConfigFile }) { require.resolve('./test_suites/panel_actions'), require.resolve('./test_suites/embeddable_explorer'), require.resolve('./test_suites/core_plugins'), + require.resolve('./test_suites/management'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/core_app_status/kibana.json b/test/plugin_functional/plugins/core_app_status/kibana.json new file mode 100644 index 0000000000000..91d8e6fd8f9e1 --- /dev/null +++ b/test/plugin_functional/plugins/core_app_status/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "core_app_status", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["core_app_status"], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/core_app_status/package.json b/test/plugin_functional/plugins/core_app_status/package.json new file mode 100644 index 0000000000000..61655487c6acb --- /dev/null +++ b/test/plugin_functional/plugins/core_app_status/package.json @@ -0,0 +1,17 @@ +{ + "name": "core_app_status", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_app_status", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/core_app_status/public/application.tsx b/test/plugin_functional/plugins/core_app_status/public/application.tsx new file mode 100644 index 0000000000000..323774392a6d7 --- /dev/null +++ b/test/plugin_functional/plugins/core_app_status/public/application.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +import { AppMountContext, AppMountParameters } from 'kibana/public'; + +const AppStatusApp = () => ( + + + + + +

Welcome to App Status Test App!

+
+
+
+ + + + +

App Status Test App home page section title

+
+
+
+ App Status Test App content +
+
+
+); + +export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { + render(, element); + + return () => unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_app_status/public/index.ts b/test/plugin_functional/plugins/core_app_status/public/index.ts new file mode 100644 index 0000000000000..e0ad7c25a54b8 --- /dev/null +++ b/test/plugin_functional/plugins/core_app_status/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { CoreAppStatusPlugin, CoreAppStatusPluginSetup, CoreAppStatusPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new CoreAppStatusPlugin(); diff --git a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx new file mode 100644 index 0000000000000..85caaaf5f9090 --- /dev/null +++ b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx @@ -0,0 +1,56 @@ +/* + * 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 { Plugin, CoreSetup, AppUpdater, AppUpdatableFields, CoreStart } from 'kibana/public'; +import { BehaviorSubject } from 'rxjs'; + +export class CoreAppStatusPlugin + implements Plugin { + private appUpdater = new BehaviorSubject(() => ({})); + + public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'app_status', + title: 'App Status', + euiIconType: 'snowflake', + updater$: this.appUpdater, + async mount(context, params) { + const { renderApp } = await import('./application'); + return renderApp(context, params); + }, + }); + + return {}; + } + + public start(core: CoreStart) { + return { + setAppStatus: (status: Partial) => { + this.appUpdater.next(() => status); + }, + navigateToApp: async (appId: string) => { + return core.application.navigateToApp(appId); + }, + }; + } + public stop() {} +} + +export type CoreAppStatusPluginSetup = ReturnType; +export type CoreAppStatusPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_app_status/tsconfig.json b/test/plugin_functional/plugins/core_app_status/tsconfig.json new file mode 100644 index 0000000000000..5fcaeafbb0d85 --- /dev/null +++ b/test/plugin_functional/plugins/core_app_status/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/core_plugin_appleave/kibana.json b/test/plugin_functional/plugins/core_plugin_appleave/kibana.json new file mode 100644 index 0000000000000..95343cbcf2804 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_appleave/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "core_plugin_appleave", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["core_plugin_appleave"], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/core_plugin_appleave/package.json b/test/plugin_functional/plugins/core_plugin_appleave/package.json new file mode 100644 index 0000000000000..e0488655a1723 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_appleave/package.json @@ -0,0 +1,17 @@ +{ + "name": "core_plugin_appleave", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_plugin_appleave", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/core_plugin_appleave/public/application.tsx b/test/plugin_functional/plugins/core_plugin_appleave/public/application.tsx new file mode 100644 index 0000000000000..d0b024f90c737 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_appleave/public/application.tsx @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +import { AppMountParameters } from 'kibana/public'; + +const App = ({ appName }: { appName: string }) => ( + + + + + +

Welcome to {appName}!

+
+
+
+ + + + +

{appName} home page section title

+
+
+
+ {appName} page content +
+
+
+); + +export const renderApp = (appName: string, { element }: AppMountParameters) => { + render(, element); + return () => unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_plugin_appleave/public/index.ts b/test/plugin_functional/plugins/core_plugin_appleave/public/index.ts new file mode 100644 index 0000000000000..3eb2279aa9166 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_appleave/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { CoreAppLeavePlugin, CoreAppLeavePluginSetup, CoreAppLeavePluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new CoreAppLeavePlugin(); diff --git a/test/plugin_functional/plugins/core_plugin_appleave/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_appleave/public/plugin.tsx new file mode 100644 index 0000000000000..336bb9d787895 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_appleave/public/plugin.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup } from 'kibana/public'; + +export class CoreAppLeavePlugin + implements Plugin { + public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'appleave1', + title: 'AppLeave 1', + async mount(context, params) { + const { renderApp } = await import('./application'); + params.onAppLeave(actions => actions.confirm('confirm-message', 'confirm-title')); + return renderApp('AppLeave 1', params); + }, + }); + core.application.register({ + id: 'appleave2', + title: 'AppLeave 2', + async mount(context, params) { + const { renderApp } = await import('./application'); + params.onAppLeave(actions => actions.default()); + return renderApp('AppLeave 2', params); + }, + }); + + return {}; + } + + public start() {} + public stop() {} +} + +export type CoreAppLeavePluginSetup = ReturnType; +export type CoreAppLeavePluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json new file mode 100644 index 0000000000000..5fcaeafbb0d85 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/management_test_plugin/kibana.json b/test/plugin_functional/plugins/management_test_plugin/kibana.json new file mode 100644 index 0000000000000..e52b60b3a4e31 --- /dev/null +++ b/test/plugin_functional/plugins/management_test_plugin/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "management_test_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["management_test_plugin"], + "server": false, + "ui": true, + "requiredPlugins": ["management"] +} diff --git a/test/plugin_functional/plugins/management_test_plugin/package.json b/test/plugin_functional/plugins/management_test_plugin/package.json new file mode 100644 index 0000000000000..656d92e9eb1f7 --- /dev/null +++ b/test/plugin_functional/plugins/management_test_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "management_test_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/management_test_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/test/plugin_functional/plugins/management_test_plugin/public/index.ts b/test/plugin_functional/plugins/management_test_plugin/public/index.ts new file mode 100644 index 0000000000000..1efcc6cd3bbd6 --- /dev/null +++ b/test/plugin_functional/plugins/management_test_plugin/public/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { + ManagementTestPlugin, + ManagementTestPluginSetup, + ManagementTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer = () => + new ManagementTestPlugin(); diff --git a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx new file mode 100644 index 0000000000000..8b7cdd653ed8c --- /dev/null +++ b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx @@ -0,0 +1,73 @@ +/* + * 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 * as React from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter as Router, Switch, Route, Link } from 'react-router-dom'; +import { CoreSetup, Plugin } from 'kibana/public'; +import { ManagementSetup } from '../../../../../src/plugins/management/public'; + +export class ManagementTestPlugin + implements Plugin { + public setup(core: CoreSetup, { management }: { management: ManagementSetup }) { + const testSection = management.sections.register({ + id: 'test-section', + title: 'Test Section', + euiIconType: 'logoKibana', + order: 25, + }); + + testSection!.registerApp({ + id: 'test-management', + title: 'Management Test', + mount(params) { + params.setBreadcrumbs([{ text: 'Management Test' }]); + ReactDOM.render( + +

Hello from management test plugin

+ + + + Link to /one + + + + + Link to basePath + + + +
, + params.element + ); + + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }); + return {}; + } + + public start() {} + public stop() {} +} + +export type ManagementTestPluginSetup = ReturnType; +export type ManagementTestPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json new file mode 100644 index 0000000000000..5fcaeafbb0d85 --- /dev/null +++ b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.ts b/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.ts new file mode 100644 index 0000000000000..d164c2e0bc369 --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.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 url from 'url'; +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +const getKibanaUrl = (pathname?: string, search?: string) => + url.format({ + protocol: 'http:', + hostname: process.env.TEST_KIBANA_HOST || 'localhost', + port: process.env.TEST_KIBANA_PORT || '5620', + pathname, + search, + }); + +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('application using leave confirmation', () => { + describe('when navigating to another app', () => { + it('prevents navigation if user click cancel on the confirmation dialog', async () => { + await PageObjects.common.navigateToApp('appleave1'); + await appsMenu.clickLink('AppLeave 2'); + + await testSubjects.existOrFail('appLeaveConfirmModal'); + await PageObjects.common.clickCancelOnModal(false); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/appleave1')); + }); + it('allows navigation if user click confirm on the confirmation dialog', async () => { + await PageObjects.common.navigateToApp('appleave1'); + await appsMenu.clickLink('AppLeave 2'); + + await testSubjects.existOrFail('appLeaveConfirmModal'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/appleave2')); + }); + }); + + describe('when navigating to a legacy app', () => { + it('prevents navigation if user click cancel on the alert dialog', async () => { + await PageObjects.common.navigateToApp('appleave1'); + await appsMenu.clickLink('Core Legacy Compat'); + + const alert = await browser.getAlert(); + expect(alert).not.to.eql(undefined); + alert!.dismiss(); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/appleave1')); + }); + it('allows navigation if user click leave on the alert dialog', async () => { + await PageObjects.common.navigateToApp('appleave1'); + await appsMenu.clickLink('Core Legacy Compat'); + + const alert = await browser.getAlert(); + expect(alert).not.to.eql(undefined); + alert!.accept(); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/core_plugin_legacy')); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts new file mode 100644 index 0000000000000..703ae30533bae --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { + AppNavLinkStatus, + AppStatus, + AppUpdatableFields, +} from '../../../../src/core/public/application/types'; +import { PluginFunctionalProviderContext } from '../../services'; +import { CoreAppStatusPluginStart } from '../../plugins/core_app_status/public/plugin'; +import '../../plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + + const setAppStatus = async (s: Partial) => { + await browser.executeAsync(async (status: Partial, cb: Function) => { + const plugin = window.__coreProvider.start.plugins + .core_app_status as CoreAppStatusPluginStart; + plugin.setAppStatus(status); + cb(); + }, s); + }; + + const navigateToApp = async (i: string): Promise<{ error?: string }> => { + return (await browser.executeAsync(async (appId, cb: Function) => { + // navigating in legacy mode performs a page refresh + // and webdriver seems to re-execute the script after the reload + // as it considers it didn't end on the previous session. + // however when testing navigation to NP app, __coreProvider is not accessible + // so we need to check for existence. + if (!window.__coreProvider) { + cb({}); + } + const plugin = window.__coreProvider.start.plugins + .core_app_status as CoreAppStatusPluginStart; + try { + await plugin.navigateToApp(appId); + cb({}); + } catch (e) { + cb({ + error: e.message, + }); + } + }, i)) as any; + }; + + describe('application status management', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('settings'); + }); + + it('can change the navLink status at runtime', async () => { + await setAppStatus({ + navLinkStatus: AppNavLinkStatus.disabled, + }); + let link = await appsMenu.getLink('App Status'); + expect(link).not.to.eql(undefined); + expect(link!.disabled).to.eql(true); + + await setAppStatus({ + navLinkStatus: AppNavLinkStatus.hidden, + }); + link = await appsMenu.getLink('App Status'); + expect(link).to.eql(undefined); + + await setAppStatus({ + navLinkStatus: AppNavLinkStatus.visible, + tooltip: 'Some tooltip', + }); + link = await appsMenu.getLink('Some tooltip'); // the tooltip replaces the name in the selector we use. + expect(link).not.to.eql(undefined); + expect(link!.disabled).to.eql(false); + }); + + it('shows an error when navigating to an inaccessible app', async () => { + await setAppStatus({ + status: AppStatus.inaccessible, + }); + + const result = await navigateToApp('app_status'); + expect(result.error).to.contain( + 'Trying to navigate to an inaccessible application: app_status' + ); + }); + + it('allows to navigate to an accessible app', async () => { + await setAppStatus({ + status: AppStatus.accessible, + }); + + const result = await navigateToApp('app_status'); + expect(result.error).to.eql(undefined); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index bf33f37694c3a..d66e2e7dc5da7 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -27,5 +27,7 @@ export default function({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./ui_plugins')); loadTestFile(require.resolve('./ui_settings')); loadTestFile(require.resolve('./top_nav')); + loadTestFile(require.resolve('./application_leave_confirm')); + loadTestFile(require.resolve('./application_status')); }); } diff --git a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js index 30b9dbe0fe80a..ef6f0a626bd15 100644 --- a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js +++ b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const renderable = getService('renderable'); - const PageObjects = getPageObjects(['common', 'visualize']); + const PageObjects = getPageObjects(['common', 'visualize', 'visEditor']); async function getCounterValue() { return await testSubjects.getVisibleText('counter'); @@ -42,9 +42,9 @@ export default function({ getService, getPageObjects }) { const editor = await testSubjects.find('counterEditor'); await editor.clearValue(); await editor.type('10'); - const isApplyEnabled = await PageObjects.visualize.isApplyEnabled(); + const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled(); expect(isApplyEnabled).to.be(true); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickGo(); const counter = await getCounterValue(); expect(counter).to.be('10'); }); @@ -57,7 +57,7 @@ export default function({ getService, getPageObjects }) { const editorValue = await getEditorValue(); expect(editorValue).to.be('11'); // If changing a param from within the vis it should immediately apply and not bring editor in an unchanged state - const isApplyEnabled = await PageObjects.visualize.isApplyEnabled(); + const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled(); expect(isApplyEnabled).to.be(false); }); }); diff --git a/src/legacy/core_plugins/kbn_doc_views/index.js b/test/plugin_functional/test_suites/management/index.js similarity index 84% rename from src/legacy/core_plugins/kbn_doc_views/index.js rename to test/plugin_functional/test_suites/management/index.js index 9078a82d63381..2bfc05547b292 100644 --- a/src/legacy/core_plugins/kbn_doc_views/index.js +++ b/test/plugin_functional/test_suites/management/index.js @@ -17,10 +17,8 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - docViews: ['plugins/kbn_doc_views/kbn_doc_views'], - }, +export default function({ loadTestFile }) { + describe('management plugin', () => { + loadTestFile(require.resolve('./management_plugin')); }); } diff --git a/test/plugin_functional/test_suites/management/management_plugin.js b/test/plugin_functional/test_suites/management/management_plugin.js new file mode 100644 index 0000000000000..d65fb1dcd3a7e --- /dev/null +++ b/test/plugin_functional/test_suites/management/management_plugin.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + describe('management plugin', function describeIndexTests() { + before(async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management'); + }); + + it('should be able to navigate to management test app', async () => { + await testSubjects.click('test-management'); + await testSubjects.existOrFail('test-management-header'); + }); + + it('should be able to navigate within management test app', async () => { + await testSubjects.click('test-management-link-one'); + await testSubjects.click('test-management-link-basepath'); + await testSubjects.existOrFail('test-management-link-one'); + }); + }); +} diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index f79fe98e07bef..2605655ed7e7a 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -8,5 +8,8 @@ node scripts/es snapshot --license=oss --download-only; echo " -> Ensuring all functional tests are in a ciGroup" yarn run grunt functionalTests:ensureAllTestsInCiGroup; -echo " -> building and extracting OSS Kibana distributable for use in functional tests" -node scripts/build --debug --oss +# Do not build kibana for code coverage run +if [[ -z "$CODE_COVERAGE" ]] ; then + echo " -> building and extracting OSS Kibana distributable for use in functional tests" + node scripts/build --debug --oss +fi diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 1cb566c908dbf..fccdb29ff512b 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -2,22 +2,30 @@ source test/scripts/jenkins_test_setup.sh -if [[ -z "$IS_PIPELINE_JOB" ]] ; then - yarn run grunt functionalTests:ensureAllTestsInCiGroup; - node scripts/build --debug --oss; -else - installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" - destDir=${installDir}-${CI_WORKER_NUMBER} - cp -R "$installDir" "$destDir" +if [[ -z "$CODE_COVERAGE" ]] ; then + if [[ -z "$IS_PIPELINE_JOB" ]] ; then + yarn run grunt functionalTests:ensureAllTestsInCiGroup; + node scripts/build --debug --oss; + else + installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" + destDir=${installDir}-${CI_WORKER_NUMBER} + cp -R "$installDir" "$destDir" - export KIBANA_INSTALL_DIR="$destDir" -fi + export KIBANA_INSTALL_DIR="$destDir" + fi + + checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; + + if [ "$CI_GROUP" == "1" ]; then + source test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh + yarn run grunt run:pluginFunctionalTestsRelease --from=source; + yarn run grunt run:exampleFunctionalTestsRelease --from=source; + yarn run grunt run:interpreterFunctionalTestsRelease; + fi +else + echo " -> Running Functional tests with code coverage" -checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; + export NODE_OPTIONS=--max_old_space_size=8192 -if [ "$CI_GROUP" == "1" ]; then - source test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh - yarn run grunt run:pluginFunctionalTestsRelease --from=source; - yarn run grunt run:exampleFunctionalTestsRelease --from=source; - yarn run grunt run:interpreterFunctionalTestsRelease; + yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; fi diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 75610884b542f..a8b5e8e4fdf97 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -4,4 +4,16 @@ set -e export TEST_BROWSER_HEADLESS=1 -"$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; +if [[ -z "$CODE_COVERAGE" ]] ; then + "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; +else + echo "NODE_ENV=$NODE_ENV" + echo " -> Running jest tests with coverage" + node scripts/jest --ci --verbose --coverage + echo "" + echo "" + echo " -> Running mocha tests with coverage" + yarn run grunt "test:mochaCoverage"; + echo "" + echo "" +fi diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 27f73c0b6e20d..e0055085d9b37 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -4,33 +4,48 @@ set -e export TEST_BROWSER_HEADLESS=1 -echo " -> Running mocha tests" -cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:browser -echo "" -echo "" - -echo " -> Running jest tests" -cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose -echo "" -echo "" - -echo " -> Running SIEM cyclic dependency test" -cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node legacy/plugins/siem/scripts/check_circular_deps -echo "" -echo "" - -# FAILING: https://github.com/elastic/kibana/issues/44250 -# echo " -> Running jest contracts tests" -# cd "$XPACK_DIR" -# SLAPSHOT_ONLINE=true CONTRACT_ONLINE=true node scripts/jest_contract.js --ci --verbose -# echo "" -# echo "" - -# echo " -> Running jest integration tests" -# cd "$XPACK_DIR" -# node scripts/jest_integration --ci --verbose -# echo "" -# echo "" +if [[ -z "$CODE_COVERAGE" ]] ; then + echo " -> Running mocha tests" + cd "$XPACK_DIR" + checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:browser + echo "" + echo "" + + echo " -> Running jest tests" + cd "$XPACK_DIR" + checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose + echo "" + echo "" + + echo " -> Running SIEM cyclic dependency test" + cd "$XPACK_DIR" + checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node legacy/plugins/siem/scripts/check_circular_deps + echo "" + echo "" + + # FAILING: https://github.com/elastic/kibana/issues/44250 + # echo " -> Running jest contracts tests" + # cd "$XPACK_DIR" + # SLAPSHOT_ONLINE=true CONTRACT_ONLINE=true node scripts/jest_contract.js --ci --verbose + # echo "" + # echo "" + + # echo " -> Running jest integration tests" + # cd "$XPACK_DIR" + # node scripts/jest_integration --ci --verbose + # echo "" + # echo "" +else + echo " -> Running jest tests with coverage" + cd "$XPACK_DIR" + # build runtime for canvas + echo "NODE_ENV=$NODE_ENV" + node ./legacy/plugins/canvas/scripts/shareable_runtime + node scripts/jest --ci --verbose --coverage + # rename file in order to be unique one + test -f ../target/kibana-coverage/jest/coverage-final.json \ + && mv ../target/kibana-coverage/jest/coverage-final.json \ + ../target/kibana-coverage/jest/xpack-coverage-final.json + echo "" + echo "" +fi \ No newline at end of file diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 9f2bafc863f41..20b12b302cb39 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -20,10 +20,13 @@ node scripts/functional_tests --assert-none-excluded \ --include-tag ciGroup9 \ --include-tag ciGroup10 -echo " -> building and extracting default Kibana distributable for use in functional tests" -cd "$KIBANA_DIR" -node scripts/build --debug --no-oss -linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" -installDir="$PARENT_DIR/install/kibana" -mkdir -p "$installDir" -tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +# Do not build kibana for code coverage run +if [[ -z "$CODE_COVERAGE" ]] ; then + echo " -> building and extracting default Kibana distributable for use in functional tests" + cd "$KIBANA_DIR" + node scripts/build --debug --no-oss + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" + installDir="$PARENT_DIR/install/kibana" + mkdir -p "$installDir" + tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +fi diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index fba05f8f252d7..58c407a848ae3 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -2,59 +2,60 @@ source test/scripts/jenkins_test_setup.sh -if [[ -z "$IS_PIPELINE_JOB" ]] ; then - echo " -> Ensuring all functional tests are in a ciGroup" +if [[ -z "$CODE_COVERAGE" ]] ; then + if [[ -z "$IS_PIPELINE_JOB" ]] ; then + echo " -> Ensuring all functional tests are in a ciGroup" + cd "$XPACK_DIR" + node scripts/functional_tests --assert-none-excluded \ + --include-tag ciGroup1 \ + --include-tag ciGroup2 \ + --include-tag ciGroup3 \ + --include-tag ciGroup4 \ + --include-tag ciGroup5 \ + --include-tag ciGroup6 \ + --include-tag ciGroup7 \ + --include-tag ciGroup8 \ + --include-tag ciGroup9 \ + --include-tag ciGroup10 + fi + + cd "$KIBANA_DIR" + + if [[ -z "$IS_PIPELINE_JOB" ]] ; then + echo " -> building and extracting default Kibana distributable for use in functional tests" + node scripts/build --debug --no-oss + + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" + installDir="$PARENT_DIR/install/kibana" + + mkdir -p "$installDir" + tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + + export KIBANA_INSTALL_DIR="$installDir" + else + installDir="$PARENT_DIR/install/kibana" + destDir="${installDir}-${CI_WORKER_NUMBER}" + cp -R "$installDir" "$destDir" + + export KIBANA_INSTALL_DIR="$destDir" + fi + + echo " -> Running functional and api tests" cd "$XPACK_DIR" - node scripts/functional_tests --assert-none-excluded \ - --include-tag ciGroup1 \ - --include-tag ciGroup2 \ - --include-tag ciGroup3 \ - --include-tag ciGroup4 \ - --include-tag ciGroup5 \ - --include-tag ciGroup6 \ - --include-tag ciGroup7 \ - --include-tag ciGroup8 \ - --include-tag ciGroup9 \ - --include-tag ciGroup10 -fi - -cd "$KIBANA_DIR" - -if [[ -z "$IS_PIPELINE_JOB" ]] ; then - echo " -> building and extracting default Kibana distributable for use in functional tests" - node scripts/build --debug --no-oss - - linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" - installDir="$PARENT_DIR/install/kibana" - mkdir -p "$installDir" - tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + checks-reporter-with-killswitch "X-Pack Chrome Functional tests / Group ${CI_GROUP}" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "ciGroup$CI_GROUP" - export KIBANA_INSTALL_DIR="$installDir" + echo "" + echo "" else - installDir="$PARENT_DIR/install/kibana" - destDir="${installDir}-${CI_WORKER_NUMBER}" - cp -R "$installDir" "$destDir" + echo " -> Running X-Pack functional tests with code coverage" + cd "$XPACK_DIR" - export KIBANA_INSTALL_DIR="$destDir" -fi + export NODE_OPTIONS=--max_old_space_size=8192 -echo " -> Running functional and api tests" -cd "$XPACK_DIR" - -checks-reporter-with-killswitch "X-Pack Chrome Functional tests / Group ${CI_GROUP}" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --include-tag "ciGroup$CI_GROUP" - -echo "" -echo "" - -# checks-reporter-with-killswitch "X-Pack Firefox Functional tests / Group ${CI_GROUP}" \ -# node scripts/functional_tests --debug --bail \ -# --kibana-install-dir "$installDir" \ -# --include-tag "ciGroup$CI_GROUP" \ -# --config "test/functional/config.firefox.js" -# echo "" -# echo "" + node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" +fi diff --git a/test/server_integration/__fixtures__/README.md b/test/server_integration/__fixtures__/README.md new file mode 100644 index 0000000000000..faf881202e55e --- /dev/null +++ b/test/server_integration/__fixtures__/README.md @@ -0,0 +1,79 @@ +# HTTP SSL Test Fixtures + +These PKCS12 files are used to test SSL with a root CA and an intermediate CA. + +The files that are provided by `@kbn/dev-utils` only use a root CA, so we need additional test files for this. + +To generate these additional test files, see the steps below. + +## Step 1. Set environment variables + +```sh +CA1='test_root_ca' +CA2='test_intermediate_ca' +EE='localhost' +``` + +## Step 2. Generate PKCS12 key stores + +Using [elasticsearch-certutil](https://www.elastic.co/guide/en/elasticsearch/reference/current/certutil.html): + +```sh +bin/elasticsearch-certutil ca --ca-dn "CN=Test Root CA" -days 18250 --out $CA1.p12 --pass castorepass +bin/elasticsearch-certutil ca --ca-dn "CN=Test Intermediate CA" -days 18250 --out $CA2.p12 --pass castorepass +bin/elasticsearch-certutil cert --ca $CA2.p12 --ca-pass castorepass --name $EE --dns $EE --out $EE.p12 --pass storepass +``` + +## Step 3. Convert PKCS12 key stores + +Using OpenSSL on macOS: + +```sh +### CONVERT P12 KEYSTORES TO PEM FILES +openssl pkcs12 -in $CA1.p12 -out $CA1.crt -nokeys -passin pass:"castorepass" -passout pass: +openssl pkcs12 -in $CA1.p12 -nocerts -passin pass:"castorepass" -passout pass:"keypass" | openssl rsa -passin pass:"keypass" -out $CA1.key + +openssl pkcs12 -in $CA2.p12 -out $CA2.crt -nokeys -passin pass:"castorepass" -passout pass: +openssl pkcs12 -in $CA2.p12 -nocerts -passin pass:"castorepass" -passout pass:"keypass" | openssl rsa -passin pass:"keypass" -out $CA2.key + +openssl pkcs12 -in $EE.p12 -out $EE.crt -clcerts -passin pass:"storepass" -passout pass: +openssl pkcs12 -in $EE.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:"keypass" -out $EE.key + +### RE-SIGN INTERMEDIATE CA CERT +mkdir -p ./tmp +openssl x509 -x509toreq -in $CA2.crt -signkey $CA2.key -out ./tmp/$CA2.csr +dd if=/dev/urandom of=./tmp/rand bs=256 count=1 +touch ./tmp/index.txt +echo "01" > ./tmp/serial +cp /System/Library/OpenSSL/openssl.cnf ./tmp/ +echo " +[ tmpcnf ] +dir = ./ +certs = ./ +new_certs_dir = ./tmp +crl_dir = ./tmp/crl +database = ./tmp/index.txt +unique_subject = no +certificate = ./$CA1.crt +serial = ./tmp/serial +crlnumber = ./tmp/crlnumber +crl = ./tmp/crl.pem +private_key = ./$CA1.key +RANDFILE = ./tmp/rand +x509_extensions = v3_ca +name_opt = ca_default +cert_opt = ca_default +default_days = 18250 +default_crl_days= 30 +default_md = sha256 +preserve = no +policy = policy_anything +" >> ./tmp/openssl.cnf + +# The next command requires user input +openssl ca -config ./tmp/openssl.cnf -name tmpcnf -in ./tmp/$CA2.csr -out $CA2.crt -verbose + +### CONVERT PEM FILES BACK TO P12 KEYSTORES +cat $CA2.key $CA2.crt $CA1.crt | openssl pkcs12 -export -name $CA2 -passout pass:"castorepass" -out $CA2.p12 +cat $EE.key $EE.crt $CA1.crt $CA2.crt | openssl pkcs12 -export -name $EE -passout pass:"storepass" -out $EE.p12 +``` diff --git a/test/server_integration/__fixtures__/index.ts b/test/server_integration/__fixtures__/index.ts new file mode 100644 index 0000000000000..40f1ddb7fa0ba --- /dev/null +++ b/test/server_integration/__fixtures__/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. + */ + +import { resolve } from 'path'; + +export const CA1_CERT_PATH = resolve(__dirname, './test_root_ca.crt'); +export const CA2_CERT_PATH = resolve(__dirname, './test_intermediate_ca.crt'); +export const EE_P12_PATH = resolve(__dirname, './localhost.p12'); +export const EE_P12_PASSWORD = 'storepass'; diff --git a/test/server_integration/__fixtures__/localhost.p12 b/test/server_integration/__fixtures__/localhost.p12 new file mode 100644 index 0000000000000..1b0d11fb88098 Binary files /dev/null and b/test/server_integration/__fixtures__/localhost.p12 differ diff --git a/test/server_integration/__fixtures__/test_intermediate_ca.crt b/test/server_integration/__fixtures__/test_intermediate_ca.crt new file mode 100644 index 0000000000000..2e143200d290a --- /dev/null +++ b/test/server_integration/__fixtures__/test_intermediate_ca.crt @@ -0,0 +1,79 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Test Root CA + Validity + Not Before: Jan 9 15:50:00 2020 GMT + Not After : Dec 27 15:50:00 2069 GMT + Subject: CN=Test Intermediate CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:ba:bb:d5:d7:5a:0a:b0:95:43:63:73:bc:1f:3f: + a4:71:3c:3f:69:96:8c:5d:e1:5b:82:95:2c:d1:b3: + 3b:7a:5c:f0:54:c9:d2:be:37:c7:81:ce:db:90:fa: + c0:0c:e8:b7:e5:51:18:29:0b:47:89:0f:b1:e3:7f: + 06:f0:fe:f6:a7:e8:42:36:58:4b:c7:04:81:48:5b: + 20:11:be:95:6b:bd:8c:0b:1b:21:2d:26:47:0b:c5: + 98:59:d7:a2:35:09:4f:1a:eb:74:d4:bc:fd:df:41: + 45:5d:fd:a6:0e:dd:02:7e:52:a4:21:9d:ac:c7:0e: + 73:50:2d:7b:6e:30:05:20:a2:ee:60:fa:0e:80:7d: + d0:0c:fd:24:ae:ef:96:70:7e:a3:bc:87:e4:fc:50: + 43:a7:a6:ef:dc:0d:7d:9e:02:73:3d:6b:b1:b3:e9: + d5:98:42:2b:ed:63:c1:a2:bb:49:19:a4:5b:d6:6e: + 33:54:44:19:f3:51:db:a4:ea:92:67:13:5e:80:bf: + 6d:1f:59:e4:f0:8c:93:10:38:54:37:8f:a6:4a:42: + 56:5f:db:d6:d5:2c:12:58:4c:42:aa:2c:19:8c:f7: + 30:51:b2:2c:29:c1:6b:29:73:bc:c6:45:63:41:90: + 80:0a:84:d5:02:0c:9c:67:cf:73:4e:62:40:51:ee: + 67:03 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + B4:48:6A:C8:21:77:A5:03:CF:48:C4:62:74:26:03:3F:BC:88:8C:92 + X509v3 Authority Key Identifier: + keyid:03:9B:FF:88:CA:33:A2:71:C5:31:51:A6:DA:15:EF:44:C2:CB:D3:9F + DirName:/CN=Test Root CA + serial:78:77:49:60:3B:E7:73:18:06:75:45:A7:8E:8E:B4:4E:E4:9A:E3:B0 + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + a3:10:97:ab:dd:43:8a:5b:c7:a6:b9:33:92:7b:61:fb:f0:3f: + 54:05:50:46:9e:62:11:d3:60:59:77:93:53:48:0b:c9:cf:bc: + c0:3c:b4:47:f4:f6:66:2c:86:76:38:3b:5d:13:77:41:ce:d7: + 16:ca:5e:29:33:1f:a7:ea:82:e4:0c:ad:f8:50:1d:54:cd:28: + a9:22:59:a8:e1:3f:05:b8:fb:5e:54:72:58:fa:a1:3e:f8:99: + bf:d6:50:99:8b:12:52:37:41:be:5f:c9:7d:04:46:8b:fd:8f: + 7f:64:a1:0d:b8:2b:ca:e9:4a:54:e2:bb:8b:39:b7:87:6f:8b: + 17:46:b4:5d:16:aa:75:5c:fb:33:29:52:51:24:7b:f2:d9:b3: + 9b:99:bf:08:6c:2c:43:8a:74:63:c1:32:ed:6b:4a:53:88:51: + c2:10:dd:92:f2:6f:af:65:f1:08:5a:cc:a6:2b:54:95:2b:2a: + a1:90:f2:eb:08:91:26:18:44:b7:49:11:09:c1:1c:aa:2d:b2: + d6:56:02:34:7a:97:fb:60:c5:1e:66:84:c0:40:6f:26:52:77: + 85:a3:ab:d5:8e:f0:d0:d0:2e:e0:6f:8a:de:72:e0:ee:96:e5: + 5d:4a:e9:c1:4c:c6:45:c7:36:6b:7a:1a:a6:64:71:9b:7c:7e: + 59:93:bd:b6 +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDEwxUZXN0 +IFJvb3QgQ0EwIBcNMjAwMTA5MTU1MDAwWhgPMjA2OTEyMjcxNTUwMDBaMB8xHTAb +BgNVBAMTFFRlc3QgSW50ZXJtZWRpYXRlIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAurvV11oKsJVDY3O8Hz+kcTw/aZaMXeFbgpUs0bM7elzwVMnS +vjfHgc7bkPrADOi35VEYKQtHiQ+x438G8P72p+hCNlhLxwSBSFsgEb6Va72MCxsh +LSZHC8WYWdeiNQlPGut01Lz930FFXf2mDt0CflKkIZ2sxw5zUC17bjAFIKLuYPoO +gH3QDP0kru+WcH6jvIfk/FBDp6bv3A19ngJzPWuxs+nVmEIr7WPBortJGaRb1m4z +VEQZ81HbpOqSZxNegL9tH1nk8IyTEDhUN4+mSkJWX9vW1SwSWExCqiwZjPcwUbIs +KcFrKXO8xkVjQZCACoTVAgycZ89zTmJAUe5nAwIDAQABo4GEMIGBMB0GA1UdDgQW +BBS0SGrIIXelA89IxGJ0JgM/vIiMkjBSBgNVHSMESzBJgBQDm/+IyjOiccUxUaba +Fe9EwsvTn6EbpBkwFzEVMBMGA1UEAxMMVGVzdCBSb290IENBghR4d0lgO+dzGAZ1 +RaeOjrRO5JrjsDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCjEJer +3UOKW8emuTOSe2H78D9UBVBGnmIR02BZd5NTSAvJz7zAPLRH9PZmLIZ2ODtdE3dB +ztcWyl4pMx+n6oLkDK34UB1UzSipIlmo4T8FuPteVHJY+qE++Jm/1lCZixJSN0G+ +X8l9BEaL/Y9/ZKENuCvK6UpU4ruLObeHb4sXRrRdFqp1XPszKVJRJHvy2bObmb8I +bCxDinRjwTLta0pTiFHCEN2S8m+vZfEIWsymK1SVKyqhkPLrCJEmGES3SREJwRyq +LbLWVgI0epf7YMUeZoTAQG8mUneFo6vVjvDQ0C7gb4recuDuluVdSunBTMZFxzZr +ehqmZHGbfH5Zk722 +-----END CERTIFICATE----- diff --git a/test/server_integration/__fixtures__/test_root_ca.crt b/test/server_integration/__fixtures__/test_root_ca.crt new file mode 100644 index 0000000000000..678c9a7467a22 --- /dev/null +++ b/test/server_integration/__fixtures__/test_root_ca.crt @@ -0,0 +1,24 @@ +Bag Attributes + friendlyName: ca + localKeyID: 54 69 6D 65 20 31 35 37 38 35 38 34 39 34 35 33 30 37 +subject=/CN=Test Root CA +issuer=/CN=Test Root CA +-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIUeHdJYDvncxgGdUWnjo60TuSa47AwDQYJKoZIhvcNAQEL +BQAwFzEVMBMGA1UEAxMMVGVzdCBSb290IENBMCAXDTIwMDEwOTE1NDkwNVoYDzIw +NjkxMjI3MTU0OTA1WjAXMRUwEwYDVQQDEwxUZXN0IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2G9Bmax5yFvdWEMleXcFK7G0ir04/sd4v +pRuqYhg+LhxlDOnd7HFtSsI2GGZaBktpL4eWOA8sAZ+eL89P3JV5WFDAuvlK8RZt +ECnPzl7Yar3nhPjNO5F1xbyHCPNSiQVYx7avkLJu3sv/okA65ON+BHYijbNNwS0/ +YtZYZWF7qR6rygXiLHcCIwWwZntBAKHGsBzxZv+28xRMUGsYWHq1PI25CRfDuVub +jC3LpAiJUTkrN5cE8Mpy6R9EH3c/qCk1I2daUKJVJhIzrUrsyNYwpCpbtrE605lK +qsRVkxoAK5i3zZRqiQ/m4FEmr0rTmbLJw09u+jIzfye2ivNiC7PZAgMBAAGjUzBR +MB0GA1UdDgQWBBQDm/+IyjOiccUxUabaFe9EwsvTnzAfBgNVHSMEGDAWgBQDm/+I +yjOiccUxUabaFe9EwsvTnzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQBclwdYhVNp3I4gTzxl0kbjya28auokp1+NUhAJ++eGGeTySEzbggHWkSSw +jXhzbwri1T+80smvj4XkbSvLzPurSxT1if4kmUDh+XApx/pQb/2l88lRdLqBCcSn +UxrdeDGPGMAYsxH/+/s2rMoaagBb4n8dayBesqIa+Nt+Mf8cqfM30pRGqk5HtoI/ +ZUZDOQ7JJc3mg1usjA3mtgsUQ88zAH9C3fMpf/I3sr2UQqaXYZlzmh3r5U9yNYyw +SV0NnaDd3BVJ4qOumTjlYalRJFDrvn+aNkyPN6XiwxA1qd/uVW5mIJhkoxPYWkG4 +M1b0sea/9IVucYYyXI+GyFNI7B5N +-----END CERTIFICATE----- diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 6928dedb9fb6f..26e00e5fce294 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -18,7 +18,7 @@ */ import { - KibanaSupertestProvider, + createKibanaSupertestProvider, KibanaSupertestWithoutAuthProvider, ElasticsearchSupertestProvider, } from './services'; @@ -30,7 +30,7 @@ export default async function({ readConfigFile }) { return { services: { ...commonConfig.get('services'), - supertest: KibanaSupertestProvider, + supertest: createKibanaSupertestProvider(), supertestWithoutAuth: KibanaSupertestWithoutAuthProvider, esSupertest: ElasticsearchSupertestProvider, }, diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index 1cf4cdf6064c1..2f2e7b778d361 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -17,12 +17,21 @@ * under the License. */ +import { readFileSync } from 'fs'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { createKibanaSupertestProvider } from '../../services'; + export default async function({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); return { testFiles: [require.resolve('./')], - services: httpConfig.get('services'), + services: { + ...httpConfig.get('services'), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + }), + }, servers: { ...httpConfig.get('servers'), kibana: { @@ -39,8 +48,8 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.key=${require.resolve('../../../dev_certs/server.key')}`, - `--server.ssl.certificate=${require.resolve('../../../dev_certs/server.crt')}`, + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, ], }, }; diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 36e68eaf8e345..20ab4a210cc7b 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -17,7 +17,10 @@ * under the License. */ -import { KibanaSupertestProvider } from '../../services'; +import { readFileSync } from 'fs'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; + +import { createKibanaSupertestProvider } from '../../services'; export default async function({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); @@ -34,8 +37,10 @@ export default async function({ readConfigFile }) { testFiles: [require.resolve('./')], services: { ...httpConfig.get('services'), - //eslint-disable-next-line new-cap - supertest: arg => KibanaSupertestProvider(arg, supertestOptions), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + options: supertestOptions, + }), }, servers: { ...httpConfig.get('servers'), @@ -54,8 +59,8 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.key=${require.resolve('../../../dev_certs/server.key')}`, - `--server.ssl.certificate=${require.resolve('../../../dev_certs/server.crt')}`, + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, `--server.ssl.redirectHttpFromPort=${redirectPort}`, ], }, diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js new file mode 100644 index 0000000000000..e220914af54f4 --- /dev/null +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -0,0 +1,56 @@ +/* + * 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 { readFileSync } from 'fs'; +import { CA_CERT_PATH, KBN_P12_PATH, KBN_P12_PASSWORD } from '@kbn/dev-utils'; +import { createKibanaSupertestProvider } from '../../services'; + +export default async function({ readConfigFile }) { + const httpConfig = await readConfigFile(require.resolve('../../config')); + + return { + testFiles: [require.resolve('./')], + services: { + ...httpConfig.get('services'), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + }), + }, + servers: { + ...httpConfig.get('servers'), + kibana: { + ...httpConfig.get('servers.kibana'), + protocol: 'https', + }, + }, + junit: { + reportName: 'Http SSL Integration Tests', + }, + esTestCluster: httpConfig.get('esTestCluster'), + kbnTestServer: { + ...httpConfig.get('kbnTestServer'), + serverArgs: [ + ...httpConfig.get('kbnTestServer.serverArgs'), + '--server.ssl.enabled=true', + `--server.ssl.keystore.path=${KBN_P12_PATH}`, + `--server.ssl.keystore.password=${KBN_P12_PASSWORD}`, + ], + }, + }; +} diff --git a/src/legacy/ui/public/utils/function.js b/test/server_integration/http/ssl_with_p12/index.js similarity index 76% rename from src/legacy/ui/public/utils/function.js rename to test/server_integration/http/ssl_with_p12/index.js index f9e7777d08bfe..700f30ddc21a9 100644 --- a/src/legacy/ui/public/utils/function.js +++ b/test/server_integration/http/ssl_with_p12/index.js @@ -17,16 +17,12 @@ * under the License. */ -import _ from 'lodash'; +export default function({ getService }) { + const supertest = getService('supertest'); -/** - * Call all of the function in an array - * - * @param {array[functions]} arr - * @return {undefined} - */ -export function callEach(arr) { - return _.map(arr, function(fn) { - return _.isFunction(fn) ? fn() : undefined; + describe('kibana server with ssl', () => { + it('handles requests using ssl with a P12 keystore', async () => { + await supertest.get('/').expect(302); + }); }); } diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js new file mode 100644 index 0000000000000..73a77425ec774 --- /dev/null +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -0,0 +1,56 @@ +/* + * 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 { readFileSync } from 'fs'; +import { CA1_CERT_PATH, CA2_CERT_PATH, EE_P12_PATH, EE_P12_PASSWORD } from '../../__fixtures__'; +import { createKibanaSupertestProvider } from '../../services'; + +export default async function({ readConfigFile }) { + const httpConfig = await readConfigFile(require.resolve('../../config')); + + return { + testFiles: [require.resolve('./')], + services: { + ...httpConfig.get('services'), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA1_CERT_PATH), readFileSync(CA2_CERT_PATH)], + }), + }, + servers: { + ...httpConfig.get('servers'), + kibana: { + ...httpConfig.get('servers.kibana'), + protocol: 'https', + }, + }, + junit: { + reportName: 'Http SSL Integration Tests', + }, + esTestCluster: httpConfig.get('esTestCluster'), + kbnTestServer: { + ...httpConfig.get('kbnTestServer'), + serverArgs: [ + ...httpConfig.get('kbnTestServer.serverArgs'), + '--server.ssl.enabled=true', + `--server.ssl.keystore.path=${EE_P12_PATH}`, + `--server.ssl.keystore.password=${EE_P12_PASSWORD}`, + ], + }, + }; +} diff --git a/typings/encode_uri_query.d.ts b/test/server_integration/http/ssl_with_p12_intermediate/index.js similarity index 73% rename from typings/encode_uri_query.d.ts rename to test/server_integration/http/ssl_with_p12_intermediate/index.js index 4bfc554624446..fb079a4e091c3 100644 --- a/typings/encode_uri_query.d.ts +++ b/test/server_integration/http/ssl_with_p12_intermediate/index.js @@ -17,8 +17,12 @@ * under the License. */ -declare module 'encode-uri-query' { - function encodeUriQuery(query: string, usePercentageSpace?: boolean): string; - // eslint-disable-next-line import/no-default-export - export default encodeUriQuery; +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('kibana server with ssl', () => { + it('handles requests using ssl with a P12 keystore that uses an intermediate CA', async () => { + await supertest.get('/').expect(302); + }); + }); } diff --git a/test/server_integration/services/index.js b/test/server_integration/services/index.js index 32a11f30b8683..4904bfc9eeef6 100644 --- a/test/server_integration/services/index.js +++ b/test/server_integration/services/index.js @@ -18,7 +18,7 @@ */ export { - KibanaSupertestProvider, + createKibanaSupertestProvider, KibanaSupertestWithoutAuthProvider, ElasticsearchSupertestProvider, } from './supertest'; diff --git a/test/server_integration/services/supertest.js b/test/server_integration/services/supertest.js index c09932b0207ac..74bb5400bc299 100644 --- a/test/server_integration/services/supertest.js +++ b/test/server_integration/services/supertest.js @@ -17,25 +17,19 @@ * under the License. */ -import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import supertestAsPromised from 'supertest-as-promised'; -export function KibanaSupertestProvider({ getService }, options) { - const config = getService('config'); - const kibanaServerUrl = options ? formatUrl(options) : formatUrl(config.get('servers.kibana')); - - const kibanaServerCert = config - .get('kbnTestServer.serverArgs') - .filter(arg => arg.startsWith('--server.ssl.certificate')) - .map(arg => arg.split('=').pop()) - .map(path => readFileSync(path)) - .shift(); +export function createKibanaSupertestProvider({ certificateAuthorities, options } = {}) { + return function({ getService }) { + const config = getService('config'); + const kibanaServerUrl = options ? formatUrl(options) : formatUrl(config.get('servers.kibana')); - return kibanaServerCert - ? supertestAsPromised.agent(kibanaServerUrl, { ca: kibanaServerCert }) - : supertestAsPromised(kibanaServerUrl); + return certificateAuthorities + ? supertestAsPromised.agent(kibanaServerUrl, { ca: certificateAuthorities }) + : supertestAsPromised(kibanaServerUrl); + }; } export function KibanaSupertestWithoutAuthProvider({ getService }) { diff --git a/vars/esSnapshots.groovy b/vars/esSnapshots.groovy new file mode 100644 index 0000000000000..884fbcdb17aeb --- /dev/null +++ b/vars/esSnapshots.groovy @@ -0,0 +1,50 @@ +def promote(snapshotVersion, snapshotId) { + def snapshotDestination = "${snapshotVersion}/archives/${snapshotId}" + def MANIFEST_URL = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${snapshotDestination}/manifest.json" + + dir('verified-manifest') { + def verifiedSnapshotFilename = 'manifest-latest-verified.json' + + sh """ + curl -O '${MANIFEST_URL}' + mv manifest.json ${verifiedSnapshotFilename} + """ + + googleStorageUpload( + credentialsId: 'kibana-ci-gcs-plugin', + bucket: "gs://kibana-ci-es-snapshots-daily/${snapshotVersion}", + pattern: verifiedSnapshotFilename, + sharedPublicly: false, + showInline: false, + ) + } + + // This would probably be more efficient if we could just copy using gsutil and specifying buckets for src and dest + // But we don't currently have access to the GCS credentials in a way that can be consumed easily from here... + dir('transfer-to-permanent') { + googleStorageDownload( + credentialsId: 'kibana-ci-gcs-plugin', + bucketUri: "gs://kibana-ci-es-snapshots-daily/${snapshotDestination}/*", + localDirectory: '.', + pathPrefix: snapshotDestination, + ) + + def manifestJson = readFile file: 'manifest.json' + writeFile( + file: 'manifest.json', + text: manifestJson.replace("kibana-ci-es-snapshots-daily/${snapshotDestination}", "kibana-ci-es-snapshots-permanent/${snapshotVersion}") + ) + + // Ideally we would have some delete logic here before uploading, + // But we don't currently have access to the GCS credentials in a way that can be consumed easily from here... + googleStorageUpload( + credentialsId: 'kibana-ci-gcs-plugin', + bucket: "gs://kibana-ci-es-snapshots-permanent/${snapshotVersion}", + pattern: '*.*', + sharedPublicly: false, + showInline: false, + ) + } +} + +return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 18f214554b444..5c6be70514c61 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -137,13 +137,8 @@ def jobRunner(label, useRamDisk, closure) { def scmVars // Try to clone from Github up to 8 times, waiting 15 secs between attempts - retry(8) { - try { - scmVars = checkout scm - } catch (ex) { - sleep 15 - throw ex - } + retryWithDelay(8, 15) { + scmVars = checkout scm } withEnv([ @@ -181,6 +176,18 @@ def uploadGcsArtifact(uploadPrefix, pattern) { ) } +def downloadCoverageArtifacts() { + def storageLocation = "gs://kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/coverage/" + def targetLocation = "/tmp/downloaded_coverage" + + sh "mkdir -p '${targetLocation}' && gsutil -m cp -r '${storageLocation}' '${targetLocation}'" +} + +def uploadCoverageArtifacts(prefix, pattern) { + def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/coverage/${prefix}" + uploadGcsArtifact(uploadPrefix, pattern) +} + def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ @@ -206,6 +213,11 @@ def withGcsArtifactUpload(workerName, closure) { } } }) + + if (env.CODE_COVERAGE) { + sh 'tar -czf kibana-coverage.tar.gz target/kibana-coverage/**/*' + uploadGcsArtifact("kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/coverage/${workerName}", 'kibana-coverage.tar.gz') + } } def publishJunit() { diff --git a/vars/retryWithDelay.groovy b/vars/retryWithDelay.groovy new file mode 100644 index 0000000000000..70d6f86a63ab2 --- /dev/null +++ b/vars/retryWithDelay.groovy @@ -0,0 +1,16 @@ +def call(retryTimes, delaySecs, closure) { + retry(retryTimes) { + try { + closure() + } catch (ex) { + sleep delaySecs + throw ex + } + } +} + +def call(retryTimes, Closure closure) { + call(retryTimes, 15, closure) +} + +return this diff --git a/webpackShims/angular.js b/webpackShims/angular.js deleted file mode 100644 index 4857f0f8975bc..0000000000000 --- a/webpackShims/angular.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -require('jquery'); -require('../node_modules/angular/angular'); -module.exports = window.angular; diff --git a/webpackShims/moment-timezone.js b/webpackShims/moment-timezone.js deleted file mode 100644 index d5e032ff21eef..0000000000000 --- a/webpackShims/moment-timezone.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -var moment = (module.exports = require('../node_modules/moment-timezone/moment-timezone')); -moment.tz.load(require('../node_modules/moment-timezone/data/packed/latest.json')); diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index cd4414b5fdebe..f38181ce56a2f 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -28,9 +28,10 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, + '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, }, coverageDirectory: '/../target/kibana-coverage/jest', - coverageReporters: ['html'], + coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ `${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`, `/dev-tools/jest/setup/polyfills.js`, diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts index 35d81ba74fa72..1da8b06e1587a 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -142,7 +142,7 @@ describe('params validation', () => { ); expect(() => { - validateParams(actionType, { refresh: 'true' }); + validateParams(actionType, { refresh: 'foo' }); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [refresh]: expected value of type [boolean] but got [string]"` ); diff --git a/x-pack/legacy/plugins/actions/server/plugin.ts b/x-pack/legacy/plugins/actions/server/plugin.ts index e8bc5d60a697b..48f99ba5135b7 100644 --- a/x-pack/legacy/plugins/actions/server/plugin.ts +++ b/x-pack/legacy/plugins/actions/server/plugin.ts @@ -69,7 +69,7 @@ export class Plugin { plugins: ActionsPluginsSetup ): Promise { const config = await this.config$.pipe(first()).toPromise(); - this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); + this.adminClient = core.elasticsearch.adminClient; this.defaultKibanaIndex = (await this.kibana$.pipe(first()).toPromise()).index; this.licenseState = new LicenseState(plugins.licensing.license$); diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 33679cf6fa422..30d34bd3b436d 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -63,6 +63,13 @@ This is the primary function for an alert type. Whenever the alert needs to exec |previousStartedAt|The previous date and time the alert type started a successful execution.| |params|Parameters for the execution. This is where the parameters you require will be passed in. (example threshold). Use alert type validation to ensure values are set before execution.| |state|State returned from previous execution. This is the alert level state. What the executor returns will be serialized and provided here at the next execution.| +|alertId|The id of this alert.| +|spaceId|The id of the space of this alert.| +|namespace|The namespace of the space of this alert; same as spaceId, unless spaceId === 'default', then namespace = undefined.| +|name|The name of this alert.| +|tags|The tags associated with this alert.| +|createdBy|The userid that created this alert.| +|updatedBy|The userid that last updated this alert.| ### Example diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts index ede95f76bf811..fb16f579d4c70 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.ts @@ -5,7 +5,7 @@ */ import Hapi from 'hapi'; -import { first } from 'rxjs/operators'; + import { Services } from './types'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry } from './alert_type_registry'; @@ -62,7 +62,7 @@ export class Plugin { core: AlertingCoreSetup, plugins: AlertingPluginsSetup ): Promise { - this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); + this.adminClient = core.elasticsearch.adminClient; this.licenseState = new LicenseState(plugins.licensing.license$); diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts index 10627c655eca8..87fa33a9cea58 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts @@ -75,6 +75,10 @@ describe('Task Runner', () => { enabled: true, alertTypeId: '123', schedule: { interval: '10s' }, + name: 'alert-name', + tags: ['alert-', '-tags'], + createdBy: 'alert-creator', + updatedBy: 'alert-updater', mutedInstanceIds: [], params: { bar: true, @@ -138,6 +142,10 @@ describe('Task Runner', () => { `); expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`); expect(call.state).toMatchInlineSnapshot(`Object {}`); + expect(call.name).toBe('alert-name'); + expect(call.tags).toEqual(['alert-', '-tags']); + expect(call.createdBy).toBe('alert-creator'); + expect(call.updatedBy).toBe('alert-updater'); expect(call.services.alertInstanceFactory).toBeTruthy(); expect(call.services.callCluster).toBeTruthy(); expect(call.services).toBeTruthy(); diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts index 2347e9e608ed9..42c332e82e034 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts @@ -13,7 +13,7 @@ import { createExecutionHandler } from './create_execution_handler'; import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; import { getNextRunAt } from './get_next_run_at'; import { validateAlertTypeParams } from '../lib'; -import { AlertType, RawAlert, IntervalSchedule, Services, State } from '../types'; +import { AlertType, RawAlert, IntervalSchedule, Services, State, AlertInfoParams } from '../types'; import { promiseResult, map } from '../lib/result_type'; type AlertInstances = Record; @@ -118,13 +118,25 @@ export class TaskRunner { async executeAlertInstances( services: Services, - { params, throttle, muteAll, mutedInstanceIds }: SavedObject['attributes'], - executionHandler: ReturnType + alertInfoParams: AlertInfoParams, + executionHandler: ReturnType, + spaceId: string ): Promise { + const { + params, + throttle, + muteAll, + mutedInstanceIds, + name, + tags, + createdBy, + updatedBy, + } = alertInfoParams; const { params: { alertId }, state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt }, } = this.taskInstance; + const namespace = this.context.spaceIdToNamespace(spaceId); const alertInstances = mapValues( alertRawInstances, @@ -141,6 +153,12 @@ export class TaskRunner { state: alertTypeState, startedAt: this.taskInstance.startedAt!, previousStartedAt, + spaceId, + namespace, + name, + tags, + createdBy, + updatedBy, }); // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object @@ -175,7 +193,7 @@ export class TaskRunner { async validateAndRunAlert( services: Services, apiKey: string | null, - attributes: SavedObject['attributes'], + attributes: RawAlert, references: SavedObject['references'] ) { const { @@ -191,7 +209,12 @@ export class TaskRunner { attributes.actions, references ); - return this.executeAlertInstances(services, { ...attributes, params }, executionHandler); + return this.executeAlertInstances( + services, + { ...attributes, params }, + executionHandler, + spaceId + ); } async run() { diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 9b03f9b02aa0a..def86cd46e590 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -32,6 +32,12 @@ export interface AlertExecutorOptions { services: AlertServices; params: Record; state: State; + spaceId: string; + namespace?: string; + name: string; + tags: string[]; + createdBy: string | null; + updatedBy: string | null; } export interface AlertType { @@ -108,6 +114,18 @@ export interface RawAlert extends SavedObjectAttributes { mutedInstanceIds: string[]; } +export type AlertInfoParams = Pick< + RawAlert, + | 'params' + | 'throttle' + | 'muteAll' + | 'mutedInstanceIds' + | 'name' + | 'tags' + | 'createdBy' + | 'updatedBy' +>; + export interface AlertingPlugin { setup: PluginSetupContract; start: PluginStartContract; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx index f7166bd0cec5c..5b7ddc2b450d6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -12,10 +12,11 @@ import { i18n } from '@kbn/i18n'; import { CytoscapeContext } from './Cytoscape'; import { FullscreenPanel } from './FullscreenPanel'; -const Container = styled('div')` +const ControlsContainer = styled('div')` left: ${theme.gutterTypes.gutterMedium}; position: absolute; top: ${theme.gutterTypes.gutterSmall}; + z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ `; const Button = styled(EuiButtonIcon)` @@ -83,7 +84,7 @@ export function Controls() { }); return ( - + - + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 16a91116ae762..cc09975a344b5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -57,7 +57,8 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const elements = Array.isArray(data) ? data : []; const license = useLicense(); const isValidPlatinumLicense = - license?.isActive && license?.type === 'platinum'; + license?.isActive && + (license?.type === 'platinum' || license?.type === 'trial'); return isValidPlatinumLicense ? ( ( + +

+ { + e.stopPropagation(); + }} + > + {i18n.translate('xpack.apm.transactionDetails.errorCount', { + defaultMessage: + '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', + values: { errorCount: count } + })} + +

+
+); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx deleted file mode 100644 index 4c3ec3ca9f308..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBadge } from '@elastic/eui'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; - -type Props = React.ComponentProps; - -export const ErrorCountBadge = ({ children, ...rest }: Props) => ( - - {children} - -); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 39e52be34a415..322ec7c422571 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -25,8 +25,9 @@ export const MaybeViewTraceLink = ({ } ); + const { rootTransaction } = waterfall; // the traceroot cannot be found, so we cannot link to it - if (!waterfall.traceRoot) { + if (!rootTransaction) { return ( {viewFullTraceButtonLabel} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index e5be12509e3c9..f8318b9ae97e6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -77,7 +77,6 @@ export function TransactionTabs({ {currentTab.key === timelineTab.key ? ( { it('should sort the marks by time', () => { @@ -21,9 +21,24 @@ describe('getAgentMarks', () => { } } as any; expect(getAgentMarks(transaction)).toEqual([ - { name: 'timeToFirstByte', us: 10000 }, - { name: 'domInteractive', us: 117000 }, - { name: 'domComplete', us: 118000 } + { + id: 'timeToFirstByte', + offset: 10000, + type: 'agentMark', + verticalLine: true + }, + { + id: 'domInteractive', + offset: 117000, + type: 'agentMark', + verticalLine: true + }, + { + id: 'domComplete', + offset: 118000, + type: 'agentMark', + verticalLine: true + } ]); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts new file mode 100644 index 0000000000000..8fd8edd7f8a72 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IWaterfallItem } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; +import { getErrorMarks } from '../get_error_marks'; + +describe('getErrorMarks', () => { + describe('returns empty array', () => { + it('when items are missing', () => { + expect(getErrorMarks([], {})).toEqual([]); + }); + it('when any error is available', () => { + const items = [ + { docType: 'span' }, + { docType: 'transaction' } + ] as IWaterfallItem[]; + expect(getErrorMarks(items, {})).toEqual([]); + }); + }); + + it('returns error marks', () => { + const items = [ + { + docType: 'error', + offset: 10, + skew: 5, + doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } + } as unknown, + { docType: 'transaction' }, + { + docType: 'error', + offset: 50, + skew: 0, + doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } + } as unknown + ] as IWaterfallItem[]; + expect( + getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' }) + ).toEqual([ + { + type: 'errorMark', + offset: 15, + verticalLine: false, + id: 1, + error: { error: { id: 1 }, service: { name: 'opbeans-java' } }, + serviceColor: 'red' + }, + { + type: 'errorMark', + offset: 50, + verticalLine: false, + id: 2, + error: { error: { id: 2 }, service: { name: 'opbeans-node' } }, + serviceColor: 'blue' + } + ]); + }); + + it('returns error marks without service color', () => { + const items = [ + { + docType: 'error', + offset: 10, + skew: 5, + doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } + } as unknown, + { docType: 'transaction' }, + { + docType: 'error', + offset: 50, + skew: 0, + doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } + } as unknown + ] as IWaterfallItem[]; + expect(getErrorMarks(items, {})).toEqual([ + { + type: 'errorMark', + offset: 15, + verticalLine: false, + id: 1, + error: { error: { id: 1 }, service: { name: 'opbeans-java' } }, + serviceColor: undefined + }, + { + type: 'errorMark', + offset: 50, + verticalLine: false, + id: 2, + error: { error: { id: 2 }, service: { name: 'opbeans-node' } }, + serviceColor: undefined + } + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts new file mode 100644 index 0000000000000..7798d716cb219 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction'; +import { Mark } from '.'; + +// Extends Mark without adding new properties to it. +export interface AgentMark extends Mark { + type: 'agentMark'; +} + +export function getAgentMarks(transaction?: Transaction): AgentMark[] { + const agent = transaction?.transaction.marks?.agent; + if (!agent) { + return []; + } + + return sortBy( + Object.entries(agent).map(([name, ms]) => ({ + type: 'agentMark', + id: name, + offset: ms * 1000, + verticalLine: true + })), + 'offset' + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts new file mode 100644 index 0000000000000..f1f0163a49d10 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.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 { isEmpty } from 'lodash'; +import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/ErrorRaw'; +import { + IWaterfallItem, + IWaterfallError, + IServiceColors +} from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { Mark } from '.'; + +export interface ErrorMark extends Mark { + type: 'errorMark'; + error: ErrorRaw; + serviceColor?: string; +} + +export const getErrorMarks = ( + items: IWaterfallItem[], + serviceColors: IServiceColors +): ErrorMark[] => { + if (isEmpty(items)) { + return []; + } + + return (items.filter( + item => item.docType === 'error' + ) as IWaterfallError[]).map(error => ({ + type: 'errorMark', + offset: error.offset + error.skew, + verticalLine: false, + id: error.doc.error.id, + error: error.doc, + serviceColor: serviceColors[error.doc.service.name] + })); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts new file mode 100644 index 0000000000000..52f811f5c3969 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Mark { + type: string; + offset: number; + verticalLine: boolean; + id: string; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index e4cb4ff62b36c..4e6a0eaf45585 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { px, unit } from '../../../../../style/variables'; // @ts-ignore -import Legend from '../../../../shared/charts/Legend'; +import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; const Legends = styled.div` diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index cc1f9dd529bce..4863d6519de07 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -101,7 +101,7 @@ export function SpanFlyout({ const dbContext = span.span.db; const httpContext = span.span.http; const spanTypes = getSpanTypes(span); - const spanHttpStatusCode = httpContext?.response.status_code; + const spanHttpStatusCode = httpContext?.response?.status_code; const spanHttpUrl = httpContext?.url?.original; const spanHttpMethod = httpContext?.method; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx index 2020b8252035b..df95577c81eff 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx @@ -27,8 +27,8 @@ import { DroppedSpansWarning } from './DroppedSpansWarning'; interface Props { onClose: () => void; transaction?: Transaction; - errorCount: number; - traceRootDuration?: number; + errorCount?: number; + rootTransactionDuration?: number; } function TransactionPropertiesTable({ @@ -49,8 +49,8 @@ function TransactionPropertiesTable({ export function TransactionFlyout({ transaction: transactionDoc, onClose, - errorCount, - traceRootDuration + errorCount = 0, + rootTransactionDuration }: Props) { if (!transactionDoc) { return null; @@ -84,7 +84,7 @@ export function TransactionFlyout({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx new file mode 100644 index 0000000000000..426088f0bb36a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Location } from 'history'; +import React from 'react'; +import { SpanFlyout } from './SpanFlyout'; +import { TransactionFlyout } from './TransactionFlyout'; +import { IWaterfall } from './waterfall_helpers/waterfall_helpers'; + +interface Props { + waterfallItemId?: string; + waterfall: IWaterfall; + location: Location; + toggleFlyout: ({ location }: { location: Location }) => void; +} +export const WaterfallFlyout: React.FC = ({ + waterfallItemId, + waterfall, + location, + toggleFlyout +}) => { + const currentItem = waterfall.items.find(item => item.id === waterfallItemId); + + if (!currentItem) { + return null; + } + + switch (currentItem.docType) { + case 'span': + const parentTransaction = + currentItem.parent?.docType === 'transaction' + ? currentItem.parent?.doc + : undefined; + + return ( + toggleFlyout({ location })} + /> + ); + case 'transaction': + return ( + toggleFlyout({ location })} + rootTransactionDuration={ + waterfall.rootTransaction?.transaction.duration.us + } + errorCount={waterfall.errorsPerTransaction[currentItem.id]} + /> + ); + default: + return null; + } +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index 8d4fab4aa8dd9..8a82547d717db 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -13,12 +13,12 @@ import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; import { asDuration } from '../../../../../../utils/formatters'; -import { ErrorCountBadge } from '../../ErrorCountBadge'; +import { ErrorCount } from '../../ErrorCount'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; -type ItemType = 'transaction' | 'span'; +type ItemType = 'transaction' | 'span' | 'error'; interface IContainerStyleProps { type: ItemType; @@ -89,24 +89,29 @@ interface IWaterfallItemProps { } function PrefixIcon({ item }: { item: IWaterfallItem }) { - if (item.docType === 'span') { - // icon for database spans - const isDbType = item.span.span.type.startsWith('db'); - if (isDbType) { - return ; + switch (item.docType) { + case 'span': { + // icon for database spans + const isDbType = item.doc.span.type.startsWith('db'); + if (isDbType) { + return ; + } + + // omit icon for other spans + return null; } - - // omit icon for other spans - return null; - } - - // icon for RUM agent transactions - if (isRumAgentName(item.transaction.agent.name)) { - return ; + case 'transaction': { + // icon for RUM agent transactions + if (isRumAgentName(item.doc.agent.name)) { + return ; + } + + // icon for other transactions + return ; + } + default: + return null; } - - // icon for other transactions - return ; } interface SpanActionToolTipProps { @@ -117,11 +122,9 @@ const SpanActionToolTip: React.FC = ({ item, children }) => { - if (item && item.docType === 'span') { + if (item?.docType === 'span') { return ( - + <>{children} ); @@ -140,9 +143,8 @@ function Duration({ item }: { item: IWaterfallItem }) { function HttpStatusCode({ item }: { item: IWaterfallItem }) { // http status code for transactions of type 'request' const httpStatusCode = - item.docType === 'transaction' && - item.transaction.transaction.type === 'request' - ? item.transaction.transaction.result + item.docType === 'transaction' && item.doc.transaction.type === 'request' + ? item.doc.transaction.result : undefined; if (!httpStatusCode) { @@ -153,14 +155,18 @@ function HttpStatusCode({ item }: { item: IWaterfallItem }) { } function NameLabel({ item }: { item: IWaterfallItem }) { - if (item.docType === 'span') { - return {item.name}; + switch (item.docType) { + case 'span': + return {item.doc.span.name}; + case 'transaction': + return ( + +
{item.doc.transaction.name}
+
+ ); + default: + return null; } - return ( - -
{item.name}
-
- ); } export function WaterfallItem({ @@ -210,24 +216,17 @@ export function WaterfallItem({ {errorCount > 0 && item.docType === 'transaction' ? ( - { - event.stopPropagation(); - }} - onClickAriaLabel={tooltipContent} - > - {errorCount} - + ) : null} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index d53b4077d9759..b48fc1cf7ca27 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -4,31 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { Component } from 'react'; +import React from 'react'; // @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { IUrlParams } from '../../../../../../context/UrlParamsContext/types'; +import { px } from '../../../../../../style/variables'; +import { history } from '../../../../../../utils/history'; // @ts-ignore import Timeline from '../../../../../shared/charts/Timeline'; +import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; +import { getAgentMarks } from '../Marks/get_agent_marks'; +import { getErrorMarks } from '../Marks/get_error_marks'; +import { WaterfallFlyout } from './WaterfallFlyout'; +import { WaterfallItem } from './WaterfallItem'; import { - APMQueryParams, - fromQuery, - toQuery -} from '../../../../../shared/Links/url_helpers'; -import { history } from '../../../../../../utils/history'; -import { AgentMark } from '../get_agent_marks'; -import { SpanFlyout } from './SpanFlyout'; -import { TransactionFlyout } from './TransactionFlyout'; -import { - IServiceColors, IWaterfall, IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; -import { WaterfallItem } from './WaterfallItem'; const Container = styled.div` transition: 0.1s padding ease; @@ -43,138 +38,105 @@ const TIMELINE_MARGINS = { bottom: 0 }; +const toggleFlyout = ({ + item, + location +}: { + item?: IWaterfallItem; + location: Location; +}) => { + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + flyoutDetailTab: undefined, + waterfallItemId: item?.id + }) + }); +}; + +const WaterfallItemsContainer = styled.div<{ + paddingTop: number; +}>` + padding-top: ${props => px(props.paddingTop)}; +`; + interface Props { - agentMarks: AgentMark[]; - urlParams: IUrlParams; + waterfallItemId?: string; waterfall: IWaterfall; location: Location; - serviceColors: IServiceColors; exceedsMax: boolean; } -export class Waterfall extends Component { - public onOpenFlyout = (item: IWaterfallItem) => { - this.setQueryParams({ - flyoutDetailTab: undefined, - waterfallItemId: String(item.id) - }); - }; +export const Waterfall: React.FC = ({ + waterfall, + exceedsMax, + waterfallItemId, + location +}) => { + const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found + const waterfallHeight = itemContainerHeight * waterfall.items.length; - public onCloseFlyout = () => { - this.setQueryParams({ - flyoutDetailTab: undefined, - waterfallItemId: undefined - }); - }; + const { serviceColors, duration } = waterfall; - public renderWaterfallItem = (item: IWaterfallItem) => { - const { serviceColors, waterfall, urlParams }: Props = this.props; + const agentMarks = getAgentMarks(waterfall.entryTransaction); + const errorMarks = getErrorMarks(waterfall.items, serviceColors); + + const renderWaterfallItem = (item: IWaterfallItem) => { + if (item.docType === 'error') { + return null; + } const errorCount = item.docType === 'transaction' - ? waterfall.errorCountByTransactionId[item.transaction.transaction.id] + ? waterfall.errorsPerTransaction[item.doc.transaction.id] : 0; return ( this.onOpenFlyout(item)} + onClick={() => toggleFlyout({ item, location })} /> ); }; - public getFlyOut = () => { - const { waterfall, urlParams } = this.props; - - const currentItem = - urlParams.waterfallItemId && - waterfall.itemsById[urlParams.waterfallItemId]; - - if (!currentItem) { - return null; - } - - switch (currentItem.docType) { - case 'span': - const parentTransaction = waterfall.getTransactionById( - currentItem.parentId - ); - - return ( - - ); - case 'transaction': - return ( - - ); - default: - return null; - } - }; - - public render() { - const { waterfall, exceedsMax } = this.props; - const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found - const waterfallHeight = itemContainerHeight * waterfall.orderedItems.length; - - return ( - - {exceedsMax ? ( - - ) : null} - - -
- {waterfall.orderedItems.map(this.renderWaterfallItem)} -
-
- - {this.getFlyOut()} -
- ); - } - - private setQueryParams(params: APMQueryParams) { - const { location } = this.props; - history.replace({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - ...params - }) - }); - } -} + return ( + + {exceedsMax && ( + + )} + + + + {waterfall.items.map(renderWaterfallItem)} + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 6f61f62167638..ece396bc4cfc4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -24,145 +24,44 @@ Object { "name": "GET /api", }, }, - "errorCountByTransactionId": Object { + "errorsCount": 1, + "errorsPerTransaction": Object { "myTransactionId1": 2, "myTransactionId2": 3, }, - "getTransactionById": [Function], - "itemsById": Object { - "mySpanIdA": Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 40498, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, + "items": Array [ + Object { + "doc": Object { "processor": Object { - "event": "span", + "event": "transaction", }, "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", + "name": "opbeans-node", }, "timestamp": Object { - "us": 1549324795824504, + "us": 1549324795784006, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795824504, - }, - "mySpanIdB": Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 41627, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { "duration": Object { - "us": 481, + "us": 49660, }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", + "id": "myTransactionId1", + "name": "GET /api", }, }, - "timestamp": 1549324795825633, - }, - "mySpanIdC": Object { - "childIds": Array [], - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 43899, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795827905, }, - "mySpanIdD": Object { - "childIds": Array [ - "myTransactionId2", - ], - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 1754, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { + Object { + "doc": Object { "parent": Object { "id": "myTransactionId1", }, @@ -189,59 +88,45 @@ Object { "id": "myTransactionId1", }, }, - "timestamp": 1549324795785760, - }, - "myTransactionId1": Object { - "childIds": Array [ - "mySpanIdD", - ], - "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", }, - "id": "myTransactionId1", - "name": "GET /api", }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, - }, - "myTransactionId2": Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - "offset": 39298, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", + "parentId": "myTransactionId1", "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdD", }, @@ -262,181 +147,403 @@ Object { "us": 8634, }, "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, "name": "Api::ProductsController#index", }, }, - }, - }, - "orderedItems": Array [ - Object { - "childIds": Array [ - "mySpanIdD", - ], "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, }, - "transaction": Object { - "duration": Object { - "us": 49660, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, Object { - "childIds": Array [ - "myTransactionId2", - ], - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 1754, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { + "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, }, - "timestamp": 1549324795785760, + "parentId": "myTransactionId2", + "skew": 0, }, Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - "offset": 39298, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", - "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "name": "Api::ProductsController#index", }, }, - }, - Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 40498, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", }, - "transaction": Object { + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, }, + "parentId": "myTransactionId2", + "skew": 0, }, - "timestamp": 1549324795824504, - }, - Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 41627, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -448,13 +555,13 @@ Object { }, "span": Object { "duration": Object { - "us": 481, + "us": 532, }, - "id": "mySpanIdB", - "name": "SELECT FROM products", + "id": "mySpanIdC", + "name": "SELECT FROM product", }, "timestamp": Object { - "us": 1549324795825633, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", @@ -463,57 +570,223 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795825633, - }, - Object { - "childIds": Array [], "docType": "span", "duration": 532, "id": "mySpanIdC", - "name": "SELECT FROM product", "offset": 43899, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "agent": Object { + "name": "ruby", + "version": "2", + }, + "error": Object { + "grouping_key": "errorGroupingKey1", + "id": "error1", + "log": Object { + "message": "error message", + }, + }, "parent": Object { - "id": "mySpanIdA", + "id": "myTransactionId1", }, "processor": Object { - "event": "span", + "event": "error", }, "service": Object { "name": "opbeans-ruby", }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, "timestamp": Object { - "us": 1549324795827905, + "us": 1549324795810000, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId2", + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, - "timestamp": 1549324795827905, + "parentId": "myTransactionId1", + "skew": 0, }, ], - "serviceColors": Object { - "opbeans-node": "#3185fc", - "opbeans-ruby": "#00b3a4", - }, - "services": Array [ - "opbeans-node", - "opbeans-ruby", - ], - "traceRoot": Object { + "rootTransaction": Object { "processor": Object { "event": "transaction", }, @@ -534,7 +807,10 @@ Object { "name": "GET /api", }, }, - "traceRootDuration": 49660, + "serviceColors": Object { + "opbeans-node": "#3185fc", + "opbeans-ruby": "#00b3a4", + }, } `; @@ -562,221 +838,24 @@ Object { "us": 8634, }, "id": "myTransactionId2", - "name": "Api::ProductsController#index", - }, - }, - "errorCountByTransactionId": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, - "getTransactionById": [Function], - "itemsById": Object { - "mySpanIdA": Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 1200, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795824504, - }, - "mySpanIdB": Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 2329, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 481, - }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795825633, - }, - "mySpanIdC": Object { - "childIds": Array [], - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 4601, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795827905, - }, - "mySpanIdD": Object { - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 0, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - }, - "timestamp": Object { - "us": 1549324795785760, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "timestamp": 1549324795785760, - }, - "myTransactionId1": Object { - "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, }, }, - }, - "myTransactionId2": Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", "name": "Api::ProductsController#index", - "offset": 0, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", - "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + }, + }, + "errorsCount": 0, + "errorsPerTransaction": Object { + "myTransactionId1": 2, + "myTransactionId2": 3, + }, + "items": Array [ + Object { + "doc": Object { "parent": Object { "id": "mySpanIdD", }, @@ -797,65 +876,26 @@ Object { "us": 8634, }, "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, "name": "Api::ProductsController#index", }, }, - }, - }, - "orderedItems": Array [ - Object { - "childIds": Array [ - "mySpanIdA", - ], "docType": "transaction", "duration": 8634, - "errorCount": 3, "id": "myTransactionId2", - "name": "Api::ProductsController#index", "offset": 0, + "parent": undefined, "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { - "parent": Object { - "id": "mySpanIdD", - }, - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795823304, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 8634, - }, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - }, - }, }, Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 1200, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { + "doc": Object { "parent": Object { "id": "myTransactionId2", }, @@ -882,19 +922,55 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795824504, - }, - Object { - "childIds": Array [], "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 2329, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -921,19 +997,90 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795825633, - }, - Object { - "childIds": Array [], "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 4601, + "duration": 481, + "id": "mySpanIdB", + "offset": 2329, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -960,16 +1107,90 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795827905, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 4601, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, }, ], - "serviceColors": Object { - "opbeans-ruby": "#3185fc", - }, - "services": Array [ - "opbeans-ruby", - ], - "traceRoot": Object { + "rootTransaction": Object { "processor": Object { "event": "transaction", }, @@ -990,30 +1211,61 @@ Object { "name": "GET /api", }, }, - "traceRootDuration": 49660, + "serviceColors": Object { + "opbeans-ruby": "#3185fc", + }, } `; exports[`waterfall_helpers getWaterfallItems should handle cyclic references 1`] = ` Array [ Object { - "childIds": Array [ - "a", - ], + "doc": Object { + "timestamp": Object { + "us": 10, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "transaction", "id": "a", "offset": 0, + "parent": undefined, "skew": 0, - "timestamp": 10, }, Object { - "childIds": Array [ - "a", - ], - "id": "a", + "doc": Object { + "parent": Object { + "id": "a", + }, + "span": Object { + "id": "b", + }, + "timestamp": Object { + "us": 20, + }, + }, + "docType": "span", + "id": "b", "offset": 10, + "parent": Object { + "doc": Object { + "timestamp": Object { + "us": 10, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "transaction", + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "skew": undefined, - "timestamp": 20, + "skew": 0, }, ] `; @@ -1021,89 +1273,280 @@ Array [ exports[`waterfall_helpers getWaterfallItems should order items correctly 1`] = ` Array [ Object { - "childIds": Array [ - "b2", - "b", - ], + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, "docType": "transaction", "duration": 9480, - "errorCount": 0, "id": "a", - "name": "APIRestController#products", "offset": 0, - "serviceName": "opbeans-java", + "parent": undefined, "skew": 0, - "timestamp": 1536763736366000, - "transaction": Object {}, }, Object { - "childIds": Array [], + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b2", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736367000, + }, + "transaction": Object { + "id": "a", + }, + }, "docType": "span", "duration": 4694, "id": "b2", - "name": "GET [0:0:0:0:0:0:0:1]", "offset": 1000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "serviceName": "opbeans-java", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, "transaction": Object { "id": "a", }, }, - "timestamp": 1536763736367000, - }, - Object { - "childIds": Array [ - "c", - ], "docType": "span", "duration": 4694, "id": "b", - "name": "GET [0:0:0:0:0:0:0:1]", "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "serviceName": "opbeans-java", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "parent": Object { + "id": "b", + }, + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736369000, + }, "transaction": Object { - "id": "a", + "id": "c", + "name": "APIRestController#productsRemote", }, }, - "timestamp": 1536763736368000, - }, - Object { - "childIds": Array [ - "d", - ], "docType": "transaction", "duration": 3581, - "errorCount": 0, "id": "c", - "name": "APIRestController#productsRemote", "offset": 3000, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "span", + "duration": 4694, + "id": "b", + "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, + "parentId": "a", + "skew": 0, + }, "parentId": "b", - "serviceName": "opbeans-java", "skew": 0, - "timestamp": 1536763736369000, - "transaction": Object {}, }, Object { - "childIds": Array [], + "doc": Object { + "parent": Object { + "id": "c", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "d", + "name": "SELECT", + }, + "timestamp": Object { + "us": 1536763736371000, + }, + "transaction": Object { + "id": "c", + }, + }, "docType": "span", "duration": 210, "id": "d", - "name": "SELECT", "offset": 5000, - "parentId": "c", - "serviceName": "opbeans-java", - "skew": 0, - "span": Object { - "transaction": Object { - "id": "c", + "parent": Object { + "doc": Object { + "parent": Object { + "id": "b", + }, + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736369000, + }, + "transaction": Object { + "id": "c", + "name": "APIRestController#productsRemote", + }, + }, + "docType": "transaction", + "duration": 3581, + "id": "c", + "offset": 3000, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "span", + "duration": 4694, + "id": "b", + "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, + "parentId": "a", + "skew": 0, }, + "parentId": "b", + "skew": 0, }, - "timestamp": 1536763736371000, + "parentId": "c", + "skew": 0, }, ] `; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 6166515fd9d38..426842bc02f51 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -11,8 +11,10 @@ import { getClockSkew, getOrderedWaterfallItems, getWaterfall, - IWaterfallItem + IWaterfallItem, + IWaterfallTransaction } from './waterfall_helpers'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; describe('waterfall_helpers', () => { describe('getWaterfall', () => { @@ -80,7 +82,7 @@ describe('waterfall_helpers', () => { }, timestamp: { us: 1549324795785760 } } as Span, - { + ({ parent: { id: 'mySpanIdD' }, processor: { event: 'transaction' }, trace: { id: 'myTraceId' }, @@ -88,10 +90,36 @@ describe('waterfall_helpers', () => { transaction: { duration: { us: 8634 }, name: 'Api::ProductsController#index', - id: 'myTransactionId2' + id: 'myTransactionId2', + marks: { + agent: { + domInteractive: 382, + domComplete: 383, + timeToFirstByte: 14 + } + } }, timestamp: { us: 1549324795823304 } - } as Transaction + } as unknown) as Transaction, + ({ + processor: { event: 'error' }, + parent: { id: 'myTransactionId1' }, + timestamp: { us: 1549324795810000 }, + trace: { id: 'myTraceId' }, + transaction: { id: 'myTransactionId1' }, + error: { + id: 'error1', + grouping_key: 'errorGroupingKey1', + log: { + message: 'error message' + } + }, + service: { name: 'opbeans-ruby' }, + agent: { + name: 'ruby', + version: '2' + } + } as unknown) as APMError ]; it('should return full waterfall', () => { @@ -107,8 +135,10 @@ describe('waterfall_helpers', () => { }, entryTransactionId ); - expect(waterfall.orderedItems.length).toBe(6); - expect(waterfall.orderedItems[0].id).toBe('myTransactionId1'); + + expect(waterfall.items.length).toBe(7); + expect(waterfall.items[0].id).toBe('myTransactionId1'); + expect(waterfall.errorsCount).toEqual(1); expect(waterfall).toMatchSnapshot(); }); @@ -125,26 +155,11 @@ describe('waterfall_helpers', () => { }, entryTransactionId ); - expect(waterfall.orderedItems.length).toBe(4); - expect(waterfall.orderedItems[0].id).toBe('myTransactionId2'); - expect(waterfall).toMatchSnapshot(); - }); - it('getTransactionById', () => { - const entryTransactionId = 'myTransactionId1'; - const errorsPerTransaction = { - myTransactionId1: 2, - myTransactionId2: 3 - }; - const waterfall = getWaterfall( - { - trace: { items: hits, exceedsMax: false }, - errorsPerTransaction - }, - entryTransactionId - ); - const transaction = waterfall.getTransactionById('myTransactionId2'); - expect(transaction!.transaction.id).toBe('myTransactionId2'); + expect(waterfall.items.length).toBe(4); + expect(waterfall.items[0].id).toBe('myTransactionId2'); + expect(waterfall.errorsCount).toEqual(0); + expect(waterfall).toMatchSnapshot(); }); }); @@ -152,84 +167,102 @@ describe('waterfall_helpers', () => { it('should order items correctly', () => { const items: IWaterfallItem[] = [ { + docType: 'span', + doc: { + parent: { id: 'c' }, + service: { name: 'opbeans-java' }, + transaction: { + id: 'c' + }, + timestamp: { us: 1536763736371000 }, + span: { + id: 'd', + name: 'SELECT' + } + } as Span, id: 'd', parentId: 'c', - serviceName: 'opbeans-java', - name: 'SELECT', duration: 210, - timestamp: 1536763736371000, offset: 0, - skew: 0, + skew: 0 + }, + { docType: 'span', - span: { + doc: { + parent: { id: 'a' }, + service: { name: 'opbeans-java' }, transaction: { - id: 'c' + id: 'a' + }, + timestamp: { us: 1536763736368000 }, + span: { + id: 'b', + name: 'GET [0:0:0:0:0:0:0:1]' } - } as Span - }, - { + } as Span, id: 'b', parentId: 'a', - serviceName: 'opbeans-java', - name: 'GET [0:0:0:0:0:0:0:1]', duration: 4694, - timestamp: 1536763736368000, offset: 0, - skew: 0, + skew: 0 + }, + { docType: 'span', - span: { + doc: { + parent: { id: 'a' }, + service: { name: 'opbeans-java' }, transaction: { id: 'a' + }, + timestamp: { us: 1536763736367000 }, + span: { + id: 'b2', + name: 'GET [0:0:0:0:0:0:0:1]' } - } as Span - }, - { + } as Span, id: 'b2', parentId: 'a', - serviceName: 'opbeans-java', - name: 'GET [0:0:0:0:0:0:0:1]', duration: 4694, - timestamp: 1536763736367000, offset: 0, - skew: 0, - docType: 'span', - span: { - transaction: { - id: 'a' - } - } as Span + skew: 0 }, { + docType: 'transaction', + doc: { + parent: { id: 'b' }, + service: { name: 'opbeans-java' }, + timestamp: { us: 1536763736369000 }, + transaction: { id: 'c', name: 'APIRestController#productsRemote' } + } as Transaction, id: 'c', parentId: 'b', - serviceName: 'opbeans-java', - name: 'APIRestController#productsRemote', duration: 3581, - timestamp: 1536763736369000, offset: 0, - skew: 0, - docType: 'transaction', - transaction: {} as Transaction, - errorCount: 0 + skew: 0 }, { + docType: 'transaction', + doc: { + service: { name: 'opbeans-java' }, + timestamp: { us: 1536763736366000 }, + transaction: { + id: 'a', + name: 'APIRestController#products' + } + } as Transaction, id: 'a', - serviceName: 'opbeans-java', - name: 'APIRestController#products', duration: 9480, - timestamp: 1536763736366000, offset: 0, - skew: 0, - docType: 'transaction', - transaction: {} as Transaction, - errorCount: 0 + skew: 0 } ]; const childrenByParentId = groupBy(items, hit => hit.parentId ? hit.parentId : 'root' ); - const entryTransactionItem = childrenByParentId.root[0]; + const entryTransactionItem = childrenByParentId + .root[0] as IWaterfallTransaction; + expect( getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) ).toMatchSnapshot(); @@ -237,13 +270,32 @@ describe('waterfall_helpers', () => { it('should handle cyclic references', () => { const items = [ - { id: 'a', timestamp: 10 } as IWaterfallItem, - { id: 'a', parentId: 'a', timestamp: 20 } as IWaterfallItem + { + docType: 'transaction', + id: 'a', + doc: ({ + transaction: { id: 'a' }, + timestamp: { us: 10 } + } as unknown) as Transaction + } as IWaterfallItem, + { + docType: 'span', + id: 'b', + parentId: 'a', + doc: ({ + span: { + id: 'b' + }, + parent: { id: 'a' }, + timestamp: { us: 20 } + } as unknown) as Span + } as IWaterfallItem ]; const childrenByParentId = groupBy(items, hit => hit.parentId ? hit.parentId : 'root' ); - const entryTransactionItem = childrenByParentId.root[0]; + const entryTransactionItem = childrenByParentId + .root[0] as IWaterfallTransaction; expect( getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) ).toMatchSnapshot(); @@ -254,12 +306,17 @@ describe('waterfall_helpers', () => { it('should adjust when child starts before parent', () => { const child = { docType: 'transaction', - timestamp: 0, + doc: { + timestamp: { us: 0 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -270,12 +327,17 @@ describe('waterfall_helpers', () => { it('should not adjust when child starts after parent has ended', () => { const child = { docType: 'transaction', - timestamp: 250, + doc: { + timestamp: { us: 250 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -286,12 +348,17 @@ describe('waterfall_helpers', () => { it('should not adjust when child starts within parent duration', () => { const child = { docType: 'transaction', - timestamp: 150, + doc: { + timestamp: { us: 150 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -305,7 +372,27 @@ describe('waterfall_helpers', () => { } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'span', + doc: { + timestamp: { us: 100 } + }, + duration: 100, + skew: 5 + } as IWaterfallItem; + + expect(getClockSkew(child, parent)).toBe(5); + }); + + it('should return parent skew for errors', () => { + const child = { + docType: 'error' + } as IWaterfallItem; + + const parent = { + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 2a69c5f51173d..1af6cddb3ba4a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -6,60 +6,52 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { + first, flatten, groupBy, - indexBy, + isEmpty, sortBy, + sum, uniq, - zipObject, - isEmpty, - first + zipObject } from 'lodash'; import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; import { Span } from '../../../../../../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction'; -interface IWaterfallIndex { - [key: string]: IWaterfallItem | undefined; -} - interface IWaterfallGroup { [key: string]: IWaterfallItem[]; } export interface IWaterfall { entryTransaction?: Transaction; - traceRoot?: Transaction; - traceRootDuration?: number; + rootTransaction?: Transaction; /** * Duration in us */ duration: number; - services: string[]; - orderedItems: IWaterfallItem[]; - itemsById: IWaterfallIndex; - getTransactionById: (id?: IWaterfallItem['id']) => Transaction | undefined; - errorCountByTransactionId: TraceAPIResponse['errorsPerTransaction']; + items: IWaterfallItem[]; + errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; + errorsCount: number; serviceColors: IServiceColors; } -interface IWaterfallItemBase { - id: string | number; +interface IWaterfallItemBase { + docType: U; + doc: T; + + id: string; + + parent?: IWaterfallItem; parentId?: string; - serviceName: string; - name: string; /** * Duration in us */ duration: number; - /** - * start timestamp in us - */ - timestamp: number; - /** * offset from first item in us */ @@ -69,53 +61,53 @@ interface IWaterfallItemBase { * skew from timestamp in us */ skew: number; - childIds?: Array; -} - -interface IWaterfallItemTransaction extends IWaterfallItemBase { - transaction: Transaction; - docType: 'transaction'; - errorCount: number; } -interface IWaterfallItemSpan extends IWaterfallItemBase { - span: Span; - docType: 'span'; -} +export type IWaterfallTransaction = IWaterfallItemBase< + Transaction, + 'transaction' +>; +export type IWaterfallSpan = IWaterfallItemBase; +export type IWaterfallError = IWaterfallItemBase; -export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction; +export type IWaterfallItem = + | IWaterfallTransaction + | IWaterfallSpan + | IWaterfallError; -function getTransactionItem( - transaction: Transaction, - errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'] -): IWaterfallItemTransaction { +function getTransactionItem(transaction: Transaction): IWaterfallTransaction { return { + docType: 'transaction', + doc: transaction, id: transaction.transaction.id, - parentId: transaction.parent && transaction.parent.id, - serviceName: transaction.service.name, - name: transaction.transaction.name, + parentId: transaction.parent?.id, duration: transaction.transaction.duration.us, - timestamp: transaction.timestamp.us, offset: 0, - skew: 0, - docType: 'transaction', - transaction, - errorCount: errorsPerTransaction[transaction.transaction.id] || 0 + skew: 0 }; } -function getSpanItem(span: Span): IWaterfallItemSpan { +function getSpanItem(span: Span): IWaterfallSpan { return { + docType: 'span', + doc: span, id: span.span.id, - parentId: span.parent && span.parent.id, - serviceName: span.service.name, - name: span.span.name, + parentId: span.parent?.id, duration: span.span.duration.us, - timestamp: span.timestamp.us, + offset: 0, + skew: 0 + }; +} + +function getErrorItem(error: APMError): IWaterfallError { + return { + docType: 'error', + doc: error, + id: error.error.id, + parentId: error.parent?.id, offset: 0, skew: 0, - docType: 'span', - span + duration: 0 }; } @@ -126,18 +118,17 @@ export function getClockSkew( if (!parentItem) { return 0; } - switch (item.docType) { - // don't calculate skew for spans. Just use parent's skew + // don't calculate skew for spans and errors. Just use parent's skew + case 'error': case 'span': return parentItem.skew; - // transaction is the inital entry in a service. Calculate skew for this, and it will be propogated to all child spans case 'transaction': { - const parentStart = parentItem.timestamp + parentItem.skew; + const parentStart = parentItem.doc.timestamp.us + parentItem.skew; // determine if child starts before the parent - const offsetStart = parentStart - item.timestamp; + const offsetStart = parentStart - item.doc.timestamp.us; if (offsetStart > 0) { const latency = Math.max(parentItem.duration - item.duration, 0) / 2; return offsetStart + latency; @@ -151,9 +142,14 @@ export function getClockSkew( export function getOrderedWaterfallItems( childrenByParentId: IWaterfallGroup, - entryTransactionItem: IWaterfallItem + entryWaterfallTransaction?: IWaterfallTransaction ) { + if (!entryWaterfallTransaction) { + return []; + } + const entryTimestamp = entryWaterfallTransaction.doc.timestamp.us; const visitedWaterfallItemSet = new Set(); + function getSortedChildren( item: IWaterfallItem, parentItem?: IWaterfallItem @@ -162,10 +158,16 @@ export function getOrderedWaterfallItems( return []; } visitedWaterfallItemSet.add(item); - const children = sortBy(childrenByParentId[item.id] || [], 'timestamp'); - item.childIds = children.map(child => child.id); - item.offset = item.timestamp - entryTransactionItem.timestamp; + const children = sortBy( + childrenByParentId[item.id] || [], + 'doc.timestamp.us' + ); + + item.parent = parentItem; + // get offset from the beginning of trace + item.offset = item.doc.timestamp.us - entryTimestamp; + // move the item to the right if it starts before its parent item.skew = getClockSkew(item, parentItem); const deepChildren = flatten( @@ -174,24 +176,21 @@ export function getOrderedWaterfallItems( return [item, ...deepChildren]; } - return getSortedChildren(entryTransactionItem); + return getSortedChildren(entryWaterfallTransaction); } -function getTraceRoot(childrenByParentId: IWaterfallGroup) { +function getRootTransaction(childrenByParentId: IWaterfallGroup) { const item = first(childrenByParentId.root); if (item && item.docType === 'transaction') { - return item.transaction; + return item.doc; } } -function getServices(items: IWaterfallItem[]) { - const serviceNames = items.map(item => item.serviceName); - return uniq(serviceNames); -} - export type IServiceColors = Record; -function getServiceColors(services: string[]) { +function getServiceColors(waterfallItems: IWaterfallItem[]) { + const services = uniq(waterfallItems.map(item => item.doc.service.name)); + const assignedColors = [ theme.euiColorVis1, theme.euiColorVis0, @@ -205,30 +204,35 @@ function getServiceColors(services: string[]) { return zipObject(services, assignedColors) as IServiceColors; } -function getDuration(items: IWaterfallItem[]) { - if (items.length === 0) { - return 0; - } - const timestampStart = items[0].timestamp; - const timestampEnd = Math.max( - ...items.map(item => item.timestamp + item.duration + item.skew) +const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) => + Math.max( + ...waterfallItems.map(item => item.offset + item.skew + item.duration), + 0 ); - return timestampEnd - timestampStart; -} -function createGetTransactionById(itemsById: IWaterfallIndex) { - return (id?: IWaterfallItem['id']) => { - if (!id) { - return undefined; +const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => + items.map(item => { + const docType = item.processor.event; + switch (docType) { + case 'span': + return getSpanItem(item as Span); + case 'transaction': + return getTransactionItem(item as Transaction); + case 'error': + return getErrorItem(item as APMError); } + }); - const item = itemsById[id]; - const isTransaction = item?.docType === 'transaction'; - if (isTransaction) { - return (item as IWaterfallItemTransaction).transaction; - } - }; -} +const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => + groupBy(waterfallItems, item => (item.parentId ? item.parentId : 'root')); + +const getEntryWaterfallTransaction = ( + entryTransactionId: string, + waterfallItems: IWaterfallItem[] +): IWaterfallTransaction | undefined => + waterfallItems.find( + item => item.docType === 'transaction' && item.id === entryTransactionId + ) as IWaterfallTransaction; export function getWaterfall( { trace, errorsPerTransaction }: TraceAPIResponse, @@ -236,59 +240,41 @@ export function getWaterfall( ): IWaterfall { if (isEmpty(trace.items) || !entryTransactionId) { return { - services: [], duration: 0, - orderedItems: [], - itemsById: {}, - getTransactionById: () => undefined, - errorCountByTransactionId: errorsPerTransaction, + items: [], + errorsPerTransaction, + errorsCount: sum(Object.values(errorsPerTransaction)), serviceColors: {} }; } - const waterfallItems = trace.items.map(traceItem => { - const docType = traceItem.processor.event; - switch (docType) { - case 'span': - return getSpanItem(traceItem as Span); - case 'transaction': - return getTransactionItem( - traceItem as Transaction, - errorsPerTransaction - ); - } - }); + const waterfallItems: IWaterfallItem[] = getWaterfallItems(trace.items); + + const childrenByParentId = getChildrenGroupedByParentId(waterfallItems); - const childrenByParentId = groupBy(waterfallItems, item => - item.parentId ? item.parentId : 'root' + const entryWaterfallTransaction = getEntryWaterfallTransaction( + entryTransactionId, + waterfallItems ); - const entryTransactionItem = waterfallItems.find( - waterfallItem => - waterfallItem.docType === 'transaction' && - waterfallItem.id === entryTransactionId + + const items = getOrderedWaterfallItems( + childrenByParentId, + entryWaterfallTransaction ); - const itemsById: IWaterfallIndex = indexBy(waterfallItems, 'id'); - const orderedItems = entryTransactionItem - ? getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) - : []; - const traceRoot = getTraceRoot(childrenByParentId); - const duration = getDuration(orderedItems); - const traceRootDuration = traceRoot && traceRoot.transaction.duration.us; - const services = getServices(orderedItems); - const getTransactionById = createGetTransactionById(itemsById); - const serviceColors = getServiceColors(services); - const entryTransaction = getTransactionById(entryTransactionId); + + const rootTransaction = getRootTransaction(childrenByParentId); + const duration = getWaterfallDuration(items); + const serviceColors = getServiceColors(items); + + const entryTransaction = entryWaterfallTransaction?.doc; return { entryTransaction, - traceRoot, - traceRootDuration, + rootTransaction, duration, - services, - orderedItems, - itemsById, - getTransactionById, - errorCountByTransactionId: errorsPerTransaction, + items, + errorsPerTransaction, + errorsCount: items.filter(item => item.docType === 'error').length, serviceColors }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts deleted file mode 100644 index af76451db68b7..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts +++ /dev/null @@ -1,27 +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 { sortBy } from 'lodash'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; - -export interface AgentMark { - name: string; - us: number; -} - -export function getAgentMarks(transaction: Transaction): AgentMark[] { - if (!(transaction.transaction.marks && transaction.transaction.marks.agent)) { - return []; - } - - return sortBy( - Object.entries(transaction.transaction.marks.agent).map(([name, ms]) => ({ - name, - us: ms * 1000 - })), - 'us' - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 2f34cc86c5cfc..77be5c999f7c3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -6,16 +6,13 @@ import { Location } from 'history'; import React from 'react'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { getAgentMarks } from './get_agent_marks'; import { ServiceLegends } from './ServiceLegends'; import { Waterfall } from './Waterfall'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; interface Props { urlParams: IUrlParams; - transaction: Transaction; location: Location; waterfall: IWaterfall; exceedsMax: boolean; @@ -24,11 +21,9 @@ interface Props { export function WaterfallContainer({ location, urlParams, - transaction, waterfall, exceedsMax }: Props) { - const agentMarks = getAgentMarks(transaction); if (!waterfall) { return null; } @@ -37,10 +32,8 @@ export function WaterfallContainer({
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx new file mode 100644 index 0000000000000..62b5f7834d3a9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { expectTextsInDocument } from '../../../../../utils/testHelpers'; +import { ErrorCount } from '../ErrorCount'; + +describe('ErrorCount', () => { + it('shows singular error message', () => { + const component = render(); + expectTextsInDocument(component, ['1 Error']); + }); + it('shows plural error message', () => { + const component = render(); + expectTextsInDocument(component, ['2 Errors']); + }); + it('prevents click propagation', () => { + const mock = jest.fn(); + const { getByText } = render( + + ); + fireEvent( + getByText('1 Error'), + new MouseEvent('click', { + bubbles: true, + cancelable: true + }) + ); + expect(mock).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index b56370a59c8e2..6dcab6c6b97c1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -5,30 +5,29 @@ */ import { + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiPagination, EuiPanel, EuiSpacer, - EuiEmptyPrompt, - EuiTitle, - EuiPagination + EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { useState, useEffect } from 'react'; -import { sum } from 'lodash'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; -import { TransactionTabs } from './TransactionTabs'; -import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { px, units } from '../../../../style/variables'; import { history } from '../../../../utils/history'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; +import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; import { MaybeViewTraceLink } from './MaybeViewTraceLink'; -import { units, px } from '../../../../style/variables'; +import { TransactionTabs } from './TransactionTabs'; +import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; const PaginationContainer = styled.div` margin-left: ${px(units.quarter)}; @@ -140,8 +139,8 @@ export const WaterfallWithSummmary: React.FC = ({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 0312e94d7ee19..eba59f6e3ce44 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -18,7 +18,7 @@ interface Props extends EuiLinkAnchorProps { children?: React.ReactNode; } -export type APMLinkExtendProps = Omit; +export type APMLinkExtendProps = Omit; export const PERSISTENT_APM_PARAMS = [ 'kuery', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 4b6355034f16a..99d8a0790a816 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -31,11 +31,15 @@ describe('SpanMetadata', () => { name: 'opbeans-java' }, span: { - id: '7efbc7056b746fcb' + id: '7efbc7056b746fcb', + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } + } } } as unknown) as Span; const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent']); + expectTextsInDocument(output, ['Service', 'Agent', 'Message']); }); }); describe('when a span is presented', () => { @@ -55,11 +59,15 @@ describe('SpanMetadata', () => { response: { status_code: 200 } }, subtype: 'http', - type: 'external' + type: 'external', + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } + } } } as unknown) as Span; const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Span']); + expectTextsInDocument(output, ['Service', 'Agent', 'Span', 'Message']); }); }); describe('when there is no id inside span', () => { @@ -83,7 +91,7 @@ describe('SpanMetadata', () => { } as unknown) as Span; const output = render(, renderOptions); expectTextsInDocument(output, ['Service', 'Agent']); - expectTextsNotInDocument(output, ['Span']); + expectTextsNotInDocument(output, ['Span', 'Message']); }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts index 7012bbcc8fcea..5a83a9bf4ef9e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts @@ -11,7 +11,8 @@ import { SPAN, LABELS, TRANSACTION, - TRACE + TRACE, + MESSAGE_SPAN } from '../sections'; export const SPAN_METADATA_SECTIONS: Section[] = [ @@ -20,5 +21,6 @@ export const SPAN_METADATA_SECTIONS: Section[] = [ TRANSACTION, TRACE, SERVICE, + MESSAGE_SPAN, AGENT ]; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 1fcb093fa0354..93e87e884ea76 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -35,6 +35,10 @@ function getTransaction() { notIncluded: 'transaction not included value', custom: { someKey: 'custom value' + }, + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } } } } as unknown) as Transaction; @@ -59,7 +63,8 @@ describe('TransactionMetadata', () => { 'Agent', 'URL', 'User', - 'Custom' + 'Custom', + 'Message' ]); }); @@ -81,7 +86,9 @@ describe('TransactionMetadata', () => { 'agent.someKey', 'url.someKey', 'user.someKey', - 'transaction.custom.someKey' + 'transaction.custom.someKey', + 'transaction.message.age.ms', + 'transaction.message.queue.name' ]); // excluded keys @@ -109,7 +116,9 @@ describe('TransactionMetadata', () => { 'agent value', 'url value', 'user value', - 'custom value' + 'custom value', + '1577958057123', + 'queue name' ]); // excluded values @@ -138,7 +147,8 @@ describe('TransactionMetadata', () => { 'Process', 'Agent', 'URL', - 'Custom' + 'Custom', + 'Message' ]); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts index 6b30c82bc35a0..18751efc6e1c1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts @@ -18,7 +18,8 @@ import { PAGE, USER, USER_AGENT, - CUSTOM_TRANSACTION + CUSTOM_TRANSACTION, + MESSAGE_TRANSACTION } from '../sections'; export const TRANSACTION_METADATA_SECTIONS: Section[] = [ @@ -29,6 +30,7 @@ export const TRANSACTION_METADATA_SECTIONS: Section[] = [ CONTAINER, SERVICE, PROCESS, + MESSAGE_TRANSACTION, AGENT, URL, { ...PAGE, key: 'transaction.page' }, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts index 403663ce2095a..ac8e9559357e3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts @@ -136,3 +136,20 @@ export const CUSTOM_TRANSACTION: Section = { key: 'transaction.custom', label: customLabel }; + +const messageLabel = i18n.translate( + 'xpack.apm.metadataTable.section.messageLabel', + { + defaultMessage: 'Message' + } +); + +export const MESSAGE_TRANSACTION: Section = { + key: 'transaction.message', + label: messageLabel +}; + +export const MESSAGE_SPAN: Section = { + key: 'span.message', + label: messageLabel +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx similarity index 51% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 964debbedb2e4..7558f002c0afc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -6,28 +6,25 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { EuiBadge } from '@elastic/eui'; +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import { px } from '../../../../public/style/variables'; -import { ErrorCountBadge } from '../../app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge'; import { units } from '../../../style/variables'; interface Props { count: number; } -const Badge = styled(ErrorCountBadge)` +const Badge = styled(EuiBadge)` margin-top: ${px(units.eighth)}; `; -const ErrorCountSummaryItem = ({ count }: Props) => { - return ( - - {i18n.translate('xpack.apm.transactionDetails.errorCount', { - defaultMessage: - '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', - values: { errorCount: count } - })} - - ); -}; - -export { ErrorCountSummaryItem }; +export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( + + {i18n.translate('xpack.apm.transactionDetails.errorCount', { + defaultMessage: + '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', + values: { errorCount: count } + })} + +); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 8b7380a18edc3..51da61cd7c1a6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -8,7 +8,7 @@ import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { Summary } from './'; import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; -import { ErrorCountSummaryItem } from './ErrorCountSummaryItem'; +import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; import { isRumAgentName } from '../../../../common/agent_name'; import { HttpInfoSummaryItem } from './HttpInfoSummaryItem'; import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; @@ -54,7 +54,7 @@ const TransactionSummary = ({ parentType="trace" />, getTransactionResultSummaryItem(transaction), - errorCount ? : null, + errorCount ? : null, transaction.user_agent ? ( ) : null diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx new file mode 100644 index 0000000000000..33f5752b6389b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ErrorCountSummaryItemBadge } from '../ErrorCountSummaryItemBadge'; +import { render } from '@testing-library/react'; +import { expectTextsInDocument } from '../../../../utils/testHelpers'; + +describe('ErrorCountSummaryItemBadge', () => { + it('shows singular error message', () => { + const component = render(); + expectTextsInDocument(component, ['1 Error']); + }); + it('shows plural error message', () => { + const component = render(); + expectTextsInDocument(component, ['2 Errors']); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx index fb087612f8e3d..6eff4759b2e7c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx @@ -65,7 +65,7 @@ export function AnnotationsPlot(props: Props) { } > - +
))} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index 848c975942ff6..c4d16c9fcf7dd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; -import Legend from '../Legend'; +import { Legend } from '../Legend'; import { unit, units, @@ -129,7 +129,7 @@ export default function Legends({ } indicator={() => (
- +
)} disabled={!showAnnotations} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index c46cbbbcccc0b..557751a0f0226 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -2725,11 +2725,14 @@ Array [ @@ -2763,11 +2766,14 @@ Array [ @@ -2794,11 +2800,14 @@ Array [ @@ -5167,11 +5176,14 @@ Array [ Avg. @@ -5210,11 +5222,14 @@ Array [ 95th @@ -5253,11 +5268,14 @@ Array [ 99th @@ -5886,11 +5904,14 @@ Array [ @@ -5924,11 +5945,14 @@ Array [ @@ -5955,11 +5979,14 @@ Array [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js deleted file mode 100644 index 601482430b00f..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js +++ /dev/null @@ -1,56 +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, { PureComponent } from 'react'; -import styled from 'styled-components'; -import { units, px, fontSizes } from '../../../../style/variables'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -const Container = styled.div` - display: flex; - align-items: center; - font-size: ${props => props.fontSize}; - color: ${theme.euiColorDarkShade}; - cursor: ${props => (props.clickable ? 'pointer' : 'initial')}; - opacity: ${props => (props.disabled ? 0.4 : 1)}; - user-select: none; -`; - -export const Indicator = styled.span` - width: ${props => px(props.radius)}; - height: ${props => px(props.radius)}; - margin-right: ${props => px(props.radius / 2)}; - background: ${props => props.color}; - border-radius: 100%; -`; - -export default class Legend extends PureComponent { - render() { - const { - onClick, - text, - color = theme.euiColorVis1, - fontSize = fontSizes.small, - radius = units.minus - 1, - disabled = false, - clickable = false, - indicator, - ...rest - } = this.props; - return ( - - {indicator ? indicator() : } - {text} - - ); - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx new file mode 100644 index 0000000000000..436b020bc9eba --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -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 theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import styled from 'styled-components'; +import { fontSizes, px, units } from '../../../../style/variables'; + +export enum Shape { + circle = 'circle', + square = 'square' +} + +interface ContainerProps { + onClick: (e: Event) => void; + fontSize?: string; + clickable: boolean; + disabled: boolean; +} +const Container = styled.div` + display: flex; + align-items: center; + font-size: ${props => props.fontSize}; + color: ${theme.euiColorDarkShade}; + cursor: ${props => (props.clickable ? 'pointer' : 'initial')}; + opacity: ${props => (props.disabled ? 0.4 : 1)}; + user-select: none; +`; + +interface IndicatorProps { + radius: number; + color: string; + shape: Shape; + withMargin: boolean; +} +export const Indicator = styled.span` + width: ${props => px(props.radius)}; + height: ${props => px(props.radius)}; + margin-right: ${props => (props.withMargin ? px(props.radius / 2) : 0)}; + background: ${props => props.color}; + border-radius: ${props => { + return props.shape === Shape.circle ? '100%' : '0'; + }}; +`; + +interface Props { + onClick?: any; + text?: string; + color?: string; + fontSize?: string; + radius?: number; + disabled?: boolean; + clickable?: boolean; + shape?: Shape; + indicator?: () => React.ReactNode; +} + +export const Legend: React.FC = ({ + onClick, + text, + color = theme.euiColorVis1, + fontSize = fontSizes.small, + radius = units.minus - 1, + disabled = false, + clickable = false, + shape = Shape.circle, + indicator, + ...rest +}) => { + return ( + + {indicator ? ( + indicator() + ) : ( + + )} + {text} + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx similarity index 56% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js rename to x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index 8ee23d61fe0eb..ffdbfe6cce7ec 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import PropTypes from 'prop-types'; import { EuiToolTip } from '@elastic/eui'; -import Legend from '../Legend'; -import { units, px } from '../../../../style/variables'; -import styled from 'styled-components'; -import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import styled from 'styled-components'; +import { px, units } from '../../../../../style/variables'; +import { asDuration } from '../../../../../utils/formatters'; +import { Legend } from '../../Legend'; +import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; const NameContainer = styled.div` border-bottom: 1px solid ${theme.euiColorMediumShade}; @@ -23,33 +23,25 @@ const TimeContainer = styled.div` padding-top: ${px(units.half)}; `; -export default function AgentMarker({ agentMark, x }) { - const legendWidth = 11; +interface Props { + mark: AgentMark; +} + +export const AgentMarker: React.FC = ({ mark }) => { return ( -
+ <> - {agentMark.name} - {asDuration(agentMark.us)} + {mark.id} + {asDuration(mark.offset)}
} >
- + ); -} - -AgentMarker.propTypes = { - agentMark: PropTypes.object.isRequired, - x: PropTypes.number.isRequired }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx new file mode 100644 index 0000000000000..51368a4fb946d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPopover, EuiText } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + TRACE_ID, + TRANSACTION_ID +} from '../../../../../../common/elasticsearch_fieldnames'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { px, unit, units } from '../../../../../style/variables'; +import { asDuration } from '../../../../../utils/formatters'; +import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; +import { Legend, Shape } from '../../Legend'; + +interface Props { + mark: ErrorMark; +} + +const Popover = styled.div` + max-width: ${px(280)}; +`; + +const TimeLegend = styled(Legend)` + margin-bottom: ${px(unit)}; +`; + +const ErrorLink = styled(ErrorDetailLink)` + display: block; + margin: ${px(units.half)} 0 ${px(units.half)} 0; +`; + +const Button = styled(Legend)` + height: 20px; + display: flex; + align-items: flex-end; +`; + +export const ErrorMarker: React.FC = ({ mark }) => { + const { urlParams } = useUrlParams(); + const [isPopoverOpen, showPopover] = useState(false); + + const togglePopover = () => showPopover(!isPopoverOpen); + + const button = ( + + )} + + ); + + await act(async () => { + wrapper.find('#invoker').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + const { title, confirmButtonText } = wrapper.find(EuiConfirmModal).props(); + expect(title).toMatchInlineSnapshot(`"Delete role mapping 'delete-me'?"`); + expect(confirmButtonText).toMatchInlineSnapshot(`"Delete role mapping"`); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); + + const notifications = toastNotifications as jest.Mocked; + expect(notifications.addError).toHaveBeenCalledTimes(0); + expect(notifications.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "deletedRoleMappingSuccessToast", + "title": "Deleted role mapping 'delete-me'", + }, + ] + `); + }); + + it('allows multiple role mappings to be deleted', async () => { + const props = { + roleMappingsAPI: ({ + deleteRoleMappings: jest.fn().mockReturnValue( + Promise.resolve([ + { + name: 'delete-me', + success: true, + }, + { + name: 'delete-me-too', + success: true, + }, + ]) + ), + } as unknown) as RoleMappingsAPI, + }; + + const roleMappingsToDelete = [ + { + name: 'delete-me', + }, + { + name: 'delete-me-too', + }, + ] as RoleMapping[]; + + const onSuccess = jest.fn(); + + const wrapper = mountWithIntl( + + {onDelete => ( + + )} + + ); + + await act(async () => { + wrapper.find('#invoker').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + const { title, confirmButtonText } = wrapper.find(EuiConfirmModal).props(); + expect(title).toMatchInlineSnapshot(`"Delete 2 role mappings?"`); + expect(confirmButtonText).toMatchInlineSnapshot(`"Delete role mappings"`); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith([ + 'delete-me', + 'delete-me-too', + ]); + const notifications = toastNotifications as jest.Mocked; + expect(notifications.addError).toHaveBeenCalledTimes(0); + expect(notifications.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "deletedRoleMappingSuccessToast", + "title": "Deleted 2 role mappings", + }, + ] + `); + }); + + it('handles mixed success/failure conditions', async () => { + const props = { + roleMappingsAPI: ({ + deleteRoleMappings: jest.fn().mockReturnValue( + Promise.resolve([ + { + name: 'delete-me', + success: true, + }, + { + name: 'i-wont-work', + success: false, + error: new Error('something went wrong. sad.'), + }, + ]) + ), + } as unknown) as RoleMappingsAPI, + }; + + const roleMappingsToDelete = [ + { + name: 'delete-me', + }, + { + name: 'i-wont-work', + }, + ] as RoleMapping[]; + + const onSuccess = jest.fn(); + + const wrapper = mountWithIntl( + + {onDelete => ( + + )} + + ); + + await act(async () => { + wrapper.find('#invoker').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith([ + 'delete-me', + 'i-wont-work', + ]); + + const notifications = toastNotifications as jest.Mocked; + expect(notifications.addError).toHaveBeenCalledTimes(0); + expect(notifications.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "deletedRoleMappingSuccessToast", + "title": "Deleted role mapping 'delete-me'", + }, + ] + `); + + expect(notifications.addDanger).toHaveBeenCalledTimes(1); + expect(notifications.addDanger.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Error deleting role mapping 'i-wont-work'", + ] + `); + }); + + it('handles errors calling the API', async () => { + const props = { + roleMappingsAPI: ({ + deleteRoleMappings: jest.fn().mockImplementation(() => { + throw new Error('AHHHHH'); + }), + } as unknown) as RoleMappingsAPI, + }; + + const roleMappingsToDelete = [ + { + name: 'delete-me', + }, + ] as RoleMapping[]; + + const onSuccess = jest.fn(); + + const wrapper = mountWithIntl( + + {onDelete => ( + + )} + + ); + + await act(async () => { + wrapper.find('#invoker').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); + + const notifications = toastNotifications as jest.Mocked; + expect(notifications.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.addSuccess).toHaveBeenCalledTimes(0); + + expect(notifications.addError).toHaveBeenCalledTimes(1); + expect(notifications.addError.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + [Error: AHHHHH], + Object { + "title": "Error deleting role mappings", + }, + ] + `); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx new file mode 100644 index 0000000000000..2072cedeab462 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useRef, useState, ReactElement } from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; +import { RoleMapping } from '../../../../../../common/model'; +import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; + +interface Props { + roleMappingsAPI: RoleMappingsAPI; + children: (deleteMappings: DeleteRoleMappings) => ReactElement; +} + +export type DeleteRoleMappings = ( + roleMappings: RoleMapping[], + onSuccess?: OnSuccessCallback +) => void; + +type OnSuccessCallback = (deletedRoleMappings: string[]) => void; + +export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI, children }) => { + const [roleMappings, setRoleMappings] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDeleteInProgress, setIsDeleteInProgress] = useState(false); + + const onSuccessCallback = useRef(null); + + const deleteRoleMappingsPrompt: DeleteRoleMappings = ( + roleMappingsToDelete, + onSuccess = () => undefined + ) => { + if (!roleMappingsToDelete || !roleMappingsToDelete.length) { + throw new Error('No Role Mappings specified for delete'); + } + setIsModalOpen(true); + setRoleMappings(roleMappingsToDelete); + onSuccessCallback.current = onSuccess; + }; + + const closeModal = () => { + setIsModalOpen(false); + setRoleMappings([]); + }; + + const deleteRoleMappings = async () => { + let result; + + setIsDeleteInProgress(true); + + try { + result = await roleMappingsAPI.deleteRoleMappings(roleMappings.map(rm => rm.name)); + } catch (e) { + toastNotifications.addError(e, { + title: i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.unknownError', + { + defaultMessage: 'Error deleting role mappings', + } + ), + }); + setIsDeleteInProgress(false); + return; + } + + setIsDeleteInProgress(false); + + closeModal(); + + const successfulDeletes = result.filter(res => res.success); + const erroredDeletes = result.filter(res => !res.success); + + // Surface success notifications + if (successfulDeletes.length > 0) { + const hasMultipleSuccesses = successfulDeletes.length > 1; + const successMessage = hasMultipleSuccesses + ? i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.successMultipleNotificationTitle', + { + defaultMessage: 'Deleted {count} role mappings', + values: { count: successfulDeletes.length }, + } + ) + : i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.successSingleNotificationTitle', + { + defaultMessage: "Deleted role mapping '{name}'", + values: { name: successfulDeletes[0].name }, + } + ); + toastNotifications.addSuccess({ + title: successMessage, + 'data-test-subj': 'deletedRoleMappingSuccessToast', + }); + if (onSuccessCallback.current) { + onSuccessCallback.current(successfulDeletes.map(({ name }) => name)); + } + } + + // Surface error notifications + if (erroredDeletes.length > 0) { + const hasMultipleErrors = erroredDeletes.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.errorMultipleNotificationTitle', + { + defaultMessage: 'Error deleting {count} role mappings', + values: { + count: erroredDeletes.length, + }, + } + ) + : i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.errorSingleNotificationTitle', + { + defaultMessage: "Error deleting role mapping '{name}'", + values: { name: erroredDeletes[0].name }, + } + ); + toastNotifications.addDanger(errorMessage); + } + }; + + const renderModal = () => { + if (!isModalOpen) { + return null; + } + + const isSingle = roleMappings.length === 1; + + return ( + + + {!isSingle ? ( + +

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

+
    + {roleMappings.map(({ name }) => ( +
  • {name}
  • + ))} +
+
+ ) : null} +
+
+ ); + }; + + return ( + + {children(deleteRoleMappingsPrompt)} + {renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts similarity index 81% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/index.ts rename to x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts index 1c84a7bc3b727..7e8b5a99c3bf5 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/index.ts +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getIndexPattern } from './get_index_pattern'; +export { DeleteProvider } from './delete_provider'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts new file mode 100644 index 0000000000000..315c1f7ec2baf --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './delete_provider'; +export * from './no_compatible_realms'; +export * from './permission_denied'; +export * from './section_loading'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts new file mode 100644 index 0000000000000..fb2e5b40c1941 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { NoCompatibleRealms } from './no_compatible_realms'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx new file mode 100644 index 0000000000000..969832b3ecbae --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { documentationLinks } from '../../services/documentation_links'; + +export const NoCompatibleRealms: React.FunctionComponent = () => ( + + } + color="warning" + iconType="alert" + > + + + + ), + }} + /> + +); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/permission_denied/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/permission_denied/index.ts new file mode 100644 index 0000000000000..8b0bc67f3f777 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/permission_denied/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PermissionDenied } from './permission_denied'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/permission_denied/permission_denied.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/permission_denied/permission_denied.tsx new file mode 100644 index 0000000000000..1a32645eaedb9 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/permission_denied/permission_denied.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiEmptyPrompt, EuiFlexGroup, EuiPageContent } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +export const PermissionDenied = () => ( + + + + + + } + body={ +

+ +

+ } + /> +
+
+); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/index.ts new file mode 100644 index 0000000000000..f59aa7a22d7c2 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SectionLoading } from './section_loading'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/section_loading.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/section_loading.test.tsx new file mode 100644 index 0000000000000..300f6ca0e1f72 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/section_loading.test.tsx @@ -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 React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { SectionLoading } from '.'; + +describe('SectionLoading', () => { + it('renders the default loading message', () => { + const wrapper = shallowWithIntl(); + expect(wrapper.props().body).toMatchInlineSnapshot(` + + + + `); + }); + + it('renders the custom message when provided', () => { + const custom =
hold your horses
; + const wrapper = shallowWithIntl({custom}); + expect(wrapper.props().body).toMatchInlineSnapshot(` + +
+ hold your horses +
+
+ `); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/section_loading.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/section_loading.tsx new file mode 100644 index 0000000000000..8ae87127ed3b2 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/section_loading/section_loading.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + children?: React.ReactChild; +} +export const SectionLoading = (props: Props) => { + return ( + } + body={ + + {props.children || ( + + )} + + } + data-test-subj="sectionLoading" + /> + ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss new file mode 100644 index 0000000000000..80e08ebcf1226 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss @@ -0,0 +1 @@ +@import './components/rule_editor_panel/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx new file mode 100644 index 0000000000000..375a8d9f374a8 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx @@ -0,0 +1,341 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; + +// brace/ace uses the Worker class, which is not currently provided by JSDOM. +// This is not required for the tests to pass, but it rather suppresses lengthy +// warnings in the console which adds unnecessary noise to the test output. +import 'test_utils/stub_web_worker'; + +import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { EditRoleMappingPage } from '.'; +import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../../components'; +import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; +import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; +import { EuiComboBox } from '@elastic/eui'; + +jest.mock('../../../../../lib/roles_api', () => { + return { + RolesApi: { + getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), + }, + }; +}); + +describe('EditRoleMappingPage', () => { + it('allows a role mapping to be created', async () => { + const roleMappingsAPI = ({ + saveRoleMapping: jest.fn().mockResolvedValue(null), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { + target: { value: 'my-role-mapping' }, + }); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'my-role-mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + all: [{ field: { username: '*' } }], + }, + metadata: {}, + }); + }); + + it('allows a role mapping to be updated', async () => { + const roleMappingsAPI = ({ + saveRoleMapping: jest.fn().mockResolvedValue(null), + getRoleMapping: jest.fn().mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + wrapper.find('button[id="addRuleOption"]').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'foo', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + any: [ + { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, + { field: { username: '*' } }, + ], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = ({ + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = ({ + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { + const roleMappingsAPI = ({ + getRoleMapping: jest.fn().mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: false, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { + const roleMappingsAPI = ({ + getRoleMapping: jest.fn().mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { source: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: false, + canUseStoredScripts: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + }); + + it('renders the visual editor by default for simple rule sets', async () => { + const roleMappingsAPI = ({ + getRoleMapping: jest.fn().mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: { + all: [ + { + field: { + username: '*', + }, + }, + { + field: { + dn: null, + }, + }, + { + field: { + realm: ['ldap', 'pki', null, 12], + }, + }, + ], + }, + }), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + }); + + it('renders the JSON editor by default for complex rule sets', async () => { + const createRule = (depth: number): Record => { + if (depth > 0) { + const rule = { + all: [ + { + field: { + username: '*', + }, + }, + ], + } as Record; + + const subRule = createRule(depth - 1); + if (subRule) { + rule.all.push(subRule); + } + + return rule; + } + return null as any; + }; + + const roleMappingsAPI = ({ + getRoleMapping: jest.fn().mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: createRule(10), + }), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx new file mode 100644 index 0000000000000..b8a75a4ad9fdf --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx @@ -0,0 +1,332 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { + EuiForm, + EuiPageContent, + EuiSpacer, + EuiText, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiLink, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { toastNotifications } from 'ui/notify'; +import { RoleMapping } from '../../../../../../common/model'; +import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { RuleEditorPanel } from './rule_editor_panel'; +import { + NoCompatibleRealms, + PermissionDenied, + DeleteProvider, + SectionLoading, +} from '../../components'; +import { ROLE_MAPPINGS_PATH } from '../../../management_urls'; +import { validateRoleMappingForSave } from '../services/role_mapping_validation'; +import { MappingInfoPanel } from './mapping_info_panel'; +import { documentationLinks } from '../../services/documentation_links'; + +interface State { + loadState: 'loading' | 'permissionDenied' | 'ready' | 'saveInProgress'; + roleMapping: RoleMapping | null; + hasCompatibleRealms: boolean; + canUseStoredScripts: boolean; + canUseInlineScripts: boolean; + formError: { + isInvalid: boolean; + error?: string; + }; + validateForm: boolean; + rulesValid: boolean; +} + +interface Props { + name?: string; + roleMappingsAPI: RoleMappingsAPI; +} + +export class EditRoleMappingPage extends Component { + constructor(props: any) { + super(props); + this.state = { + loadState: 'loading', + roleMapping: null, + hasCompatibleRealms: true, + canUseStoredScripts: true, + canUseInlineScripts: true, + rulesValid: true, + validateForm: false, + formError: { + isInvalid: false, + }, + }; + } + + public componentDidMount() { + this.loadAppData(); + } + + public render() { + const { loadState } = this.state; + + if (loadState === 'permissionDenied') { + return ; + } + + if (loadState === 'loading') { + return ( + + + + ); + } + + return ( +
+ + {this.getFormTitle()} + + this.setState({ roleMapping })} + mode={this.editingExistingRoleMapping() ? 'edit' : 'create'} + validateForm={this.state.validateForm} + canUseInlineScripts={this.state.canUseInlineScripts} + canUseStoredScripts={this.state.canUseStoredScripts} + /> + + + this.setState({ + roleMapping: { + ...this.state.roleMapping!, + rules, + }, + }) + } + /> + + {this.getFormButtons()} + +
+ ); + } + + private getFormTitle = () => { + return ( + + +

+ {this.editingExistingRoleMapping() ? ( + + ) : ( + + )} +

+
+ +

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

+
+ {!this.state.hasCompatibleRealms && ( + <> + + + + )} +
+ ); + }; + + private getFormButtons = () => { + return ( + + + + + + + + + + + + + {this.editingExistingRoleMapping() && ( + + + {deleteRoleMappingsPrompt => { + return ( + + deleteRoleMappingsPrompt([this.state.roleMapping!], () => + this.backToRoleMappingsList() + ) + } + color="danger" + > + + + ); + }} + + + )} + + ); + }; + + private onRuleValidityChange = (rulesValid: boolean) => { + this.setState({ + rulesValid, + }); + }; + + private saveRoleMapping = () => { + if (!this.state.roleMapping) { + return; + } + + const { isInvalid } = validateRoleMappingForSave(this.state.roleMapping); + if (isInvalid) { + this.setState({ validateForm: true }); + return; + } + + const roleMappingName = this.state.roleMapping.name; + + this.setState({ + loadState: 'saveInProgress', + }); + + this.props.roleMappingsAPI + .saveRoleMapping(this.state.roleMapping) + .then(() => { + toastNotifications.addSuccess({ + title: i18n.translate('xpack.security.management.editRoleMapping.saveSuccess', { + defaultMessage: `Saved role mapping '{roleMappingName}'`, + values: { + roleMappingName, + }, + }), + 'data-test-subj': 'savedRoleMappingSuccessToast', + }); + this.backToRoleMappingsList(); + }) + .catch(e => { + toastNotifications.addError(e, { + title: i18n.translate('xpack.security.management.editRoleMapping.saveError', { + defaultMessage: `Error saving role mapping`, + }), + toastMessage: e?.body?.message, + }); + + this.setState({ + loadState: 'saveInProgress', + }); + }); + }; + + private editingExistingRoleMapping = () => typeof this.props.name === 'string'; + + private async loadAppData() { + try { + const [features, roleMapping] = await Promise.all([ + this.props.roleMappingsAPI.checkRoleMappingFeatures(), + this.editingExistingRoleMapping() + ? this.props.roleMappingsAPI.getRoleMapping(this.props.name!) + : Promise.resolve({ + name: '', + enabled: true, + metadata: {}, + role_templates: [], + roles: [], + rules: {}, + }), + ]); + + const { + canManageRoleMappings, + canUseStoredScripts, + canUseInlineScripts, + hasCompatibleRealms, + } = features; + + const loadState: State['loadState'] = canManageRoleMappings ? 'ready' : 'permissionDenied'; + + this.setState({ + loadState, + hasCompatibleRealms, + canUseStoredScripts, + canUseInlineScripts, + roleMapping, + }); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.security.management.editRoleMapping.table.fetchingRoleMappingsErrorMessage', + { + defaultMessage: 'Error loading role mapping editor: {message}', + values: { message: e?.body?.message ?? '' }, + } + ), + 'data-test-subj': 'errorLoadingRoleMappingEditorToast', + }); + this.backToRoleMappingsList(); + } + } + + private backToRoleMappingsList = () => { + window.location.hash = ROLE_MAPPINGS_PATH; + }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts new file mode 100644 index 0000000000000..6758033f92d98 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditRoleMappingPage } from './edit_role_mapping_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts new file mode 100644 index 0000000000000..5042499bf00ac --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MappingInfoPanel } from './mapping_info_panel'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx new file mode 100644 index 0000000000000..d821b33ace6a7 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx @@ -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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { MappingInfoPanel } from '.'; +import { RoleMapping } from '../../../../../../../common/model'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { RoleSelector } from '../role_selector'; +import { RoleTemplateEditor } from '../role_selector/role_template_editor'; + +jest.mock('../../../../../../lib/roles_api', () => { + return { + RolesApi: { + getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), + }, + }; +}); + +describe('MappingInfoPanel', () => { + it('renders when creating a role mapping, default to the "roles" view', () => { + const props = { + roleMapping: { + name: 'my role mapping', + enabled: true, + roles: [], + role_templates: [], + rules: {}, + metadata: {}, + } as RoleMapping, + mode: 'create', + } as MappingInfoPanel['props']; + + const wrapper = mountWithIntl(); + + // Name input validation + const { value: nameInputValue, readOnly: nameInputReadOnly } = findTestSubject( + wrapper, + 'roleMappingFormNameInput' + ) + .find('input') + .props(); + + expect(nameInputValue).toEqual(props.roleMapping.name); + expect(nameInputReadOnly).toEqual(false); + + // Enabled switch validation + const { checked: enabledInputValue } = wrapper + .find('EuiSwitch[data-test-subj="roleMappingsEnabledSwitch"]') + .props(); + + expect(enabledInputValue).toEqual(props.roleMapping.enabled); + + // Verify "roles" mode + expect(wrapper.find(RoleSelector).props()).toMatchObject({ + mode: 'roles', + }); + }); + + it('renders the role templates view if templates are provided', () => { + const props = { + roleMapping: { + name: 'my role mapping', + enabled: true, + roles: [], + role_templates: [ + { + template: { + source: '', + }, + }, + ], + rules: {}, + metadata: {}, + } as RoleMapping, + mode: 'edit', + } as MappingInfoPanel['props']; + + const wrapper = mountWithIntl(); + + expect(wrapper.find(RoleSelector).props()).toMatchObject({ + mode: 'templates', + }); + }); + + it('renders a blank inline template by default when switching from roles to role templates', () => { + const props = { + roleMapping: { + name: 'my role mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: {}, + metadata: {}, + } as RoleMapping, + mode: 'create' as any, + onChange: jest.fn(), + canUseInlineScripts: true, + canUseStoredScripts: false, + validateForm: false, + }; + + const wrapper = mountWithIntl(); + + findTestSubject(wrapper, 'switchToRoleTemplatesButton').simulate('click'); + + expect(props.onChange).toHaveBeenCalledWith({ + name: 'my role mapping', + enabled: true, + roles: [], + role_templates: [ + { + template: { source: '' }, + }, + ], + rules: {}, + metadata: {}, + }); + + wrapper.setProps({ roleMapping: props.onChange.mock.calls[0][0] }); + + expect(wrapper.find(RoleTemplateEditor)).toHaveLength(1); + }); + + it('renders a blank stored template by default when switching from roles to role templates and inline scripts are disabled', () => { + const props = { + roleMapping: { + name: 'my role mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: {}, + metadata: {}, + } as RoleMapping, + mode: 'create' as any, + onChange: jest.fn(), + canUseInlineScripts: false, + canUseStoredScripts: true, + validateForm: false, + }; + + const wrapper = mountWithIntl(); + + findTestSubject(wrapper, 'switchToRoleTemplatesButton').simulate('click'); + + expect(props.onChange).toHaveBeenCalledWith({ + name: 'my role mapping', + enabled: true, + roles: [], + role_templates: [ + { + template: { id: '' }, + }, + ], + rules: {}, + metadata: {}, + }); + + wrapper.setProps({ roleMapping: props.onChange.mock.calls[0][0] }); + + expect(wrapper.find(RoleTemplateEditor)).toHaveLength(1); + }); + + it('does not create a blank role template if no script types are enabled', () => { + const props = { + roleMapping: { + name: 'my role mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: {}, + metadata: {}, + } as RoleMapping, + mode: 'create' as any, + onChange: jest.fn(), + canUseInlineScripts: false, + canUseStoredScripts: false, + validateForm: false, + }; + + const wrapper = mountWithIntl(); + + findTestSubject(wrapper, 'switchToRoleTemplatesButton').simulate('click'); + + wrapper.update(); + + expect(props.onChange).not.toHaveBeenCalled(); + expect(wrapper.find(RoleTemplateEditor)).toHaveLength(0); + }); + + it('renders the name input as readonly when editing an existing role mapping', () => { + const props = { + roleMapping: { + name: 'my role mapping', + enabled: true, + roles: [], + role_templates: [], + rules: {}, + metadata: {}, + } as RoleMapping, + mode: 'edit', + } as MappingInfoPanel['props']; + + const wrapper = mountWithIntl(); + + // Name input validation + const { value: nameInputValue, readOnly: nameInputReadOnly } = findTestSubject( + wrapper, + 'roleMappingFormNameInput' + ) + .find('input') + .props(); + + expect(nameInputValue).toEqual(props.roleMapping.name); + expect(nameInputReadOnly).toEqual(true); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx new file mode 100644 index 0000000000000..a02b4fc1709f0 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, ChangeEvent, Fragment } from 'react'; +import { + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer, + EuiDescribedFormGroup, + EuiFormRow, + EuiFieldText, + EuiLink, + EuiIcon, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RoleMapping } from '../../../../../../../common/model'; +import { + validateRoleMappingName, + validateRoleMappingRoles, + validateRoleMappingRoleTemplates, +} from '../../services/role_mapping_validation'; +import { RoleSelector } from '../role_selector'; +import { documentationLinks } from '../../../services/documentation_links'; + +interface Props { + roleMapping: RoleMapping; + onChange: (roleMapping: RoleMapping) => void; + mode: 'create' | 'edit'; + validateForm: boolean; + canUseInlineScripts: boolean; + canUseStoredScripts: boolean; +} + +interface State { + rolesMode: 'roles' | 'templates'; +} + +export class MappingInfoPanel extends Component { + constructor(props: Props) { + super(props); + this.state = { + rolesMode: + props.roleMapping.role_templates && props.roleMapping.role_templates.length > 0 + ? 'templates' + : 'roles', + }; + } + public render() { + return ( + + +

+ +

+
+ + {this.getRoleMappingName()} + {this.getEnabledSwitch()} + {this.getRolesOrRoleTemplatesSelector()} +
+ ); + } + + private getRoleMappingName = () => { + return ( + + + + } + description={ + + } + fullWidth + > + + } + fullWidth + {...(this.props.validateForm && validateRoleMappingName(this.props.roleMapping))} + > + + + + ); + }; + + private getRolesOrRoleTemplatesSelector = () => { + if (this.state.rolesMode === 'roles') { + return this.getRolesSelector(); + } + return this.getRoleTemplatesSelector(); + }; + + private getRolesSelector = () => { + const validationFunction = () => { + if (!this.props.validateForm) { + return {}; + } + return validateRoleMappingRoles(this.props.roleMapping); + }; + return ( + + + + } + description={ + + + + + + { + this.onRolesModeChange('templates'); + }} + > + + {' '} + + + + + } + fullWidth + > + + this.props.onChange(roleMapping)} + /> + + + ); + }; + + private getRoleTemplatesSelector = () => { + const validationFunction = () => { + if (!this.props.validateForm) { + return {}; + } + return validateRoleMappingRoleTemplates(this.props.roleMapping); + }; + return ( + + + + } + description={ + + + {' '} + + + + + + { + this.onRolesModeChange('roles'); + }} + data-test-subj="switchToRolesButton" + > + + {' '} + + + + + } + fullWidth + > + + this.props.onChange(roleMapping)} + /> + + + ); + }; + + private getEnabledSwitch = () => { + return ( + + + + } + description={ + + } + fullWidth + > + + } + fullWidth + > + + } + showLabel={false} + data-test-subj="roleMappingsEnabledSwitch" + checked={this.props.roleMapping.enabled} + onChange={e => { + this.props.onChange({ + ...this.props.roleMapping, + enabled: e.target.checked, + }); + }} + /> + + + ); + }; + + private onNameChange = (e: ChangeEvent) => { + const name = e.target.value; + + this.props.onChange({ + ...this.props.roleMapping, + name, + }); + }; + + private onRolesModeChange = (rolesMode: State['rolesMode']) => { + const canUseTemplates = this.props.canUseInlineScripts || this.props.canUseStoredScripts; + if (rolesMode === 'templates' && canUseTemplates) { + // Create blank template as a starting point + const defaultTemplate = this.props.canUseInlineScripts + ? { + template: { source: '' }, + } + : { + template: { id: '' }, + }; + this.props.onChange({ + ...this.props.roleMapping, + roles: [], + role_templates: [defaultTemplate], + }); + } + this.setState({ rolesMode }); + }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/add_role_template_button.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/add_role_template_button.test.tsx new file mode 100644 index 0000000000000..230664f6fc997 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/add_role_template_button.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; +import { AddRoleTemplateButton } from './add_role_template_button'; + +describe('AddRoleTemplateButton', () => { + it('renders a warning instead of a button if all script types are disabled', () => { + const wrapper = shallowWithIntl( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + } + > +

+ +

+
+ `); + }); + + it(`asks for an inline template to be created if both script types are enabled`, () => { + const onClickHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + wrapper.simulate('click'); + expect(onClickHandler).toHaveBeenCalledTimes(1); + expect(onClickHandler).toHaveBeenCalledWith('inline'); + }); + + it(`asks for a stored template to be created if inline scripts are disabled`, () => { + const onClickHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + wrapper.simulate('click'); + expect(onClickHandler).toHaveBeenCalledTimes(1); + expect(onClickHandler).toHaveBeenCalledWith('stored'); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/add_role_template_button.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/add_role_template_button.tsx new file mode 100644 index 0000000000000..5a78e399bacc7 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/add_role_template_button.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + canUseStoredScripts: boolean; + canUseInlineScripts: boolean; + onClick: (templateType: 'inline' | 'stored') => void; +} + +export const AddRoleTemplateButton = (props: Props) => { + if (!props.canUseStoredScripts && !props.canUseInlineScripts) { + return ( + + } + > +

+ +

+
+ ); + } + + const addRoleTemplate = ( + + ); + if (props.canUseInlineScripts) { + return ( + props.onClick('inline')} + data-test-subj="addRoleTemplateButton" + > + {addRoleTemplate} + + ); + } + + return ( + props.onClick('stored')} + data-test-subj="addRoleTemplateButton" + > + {addRoleTemplate} + + ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/index.tsx new file mode 100644 index 0000000000000..0011f6ea77bc6 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RoleSelector } from './role_selector'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.test.tsx new file mode 100644 index 0000000000000..89815c50e5547 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiComboBox } from '@elastic/eui'; +import { RoleSelector } from './role_selector'; +import { RoleMapping } from '../../../../../../../common/model'; +import { RoleTemplateEditor } from './role_template_editor'; +import { AddRoleTemplateButton } from './add_role_template_button'; + +jest.mock('../../../../../../lib/roles_api', () => { + return { + RolesApi: { + getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), + }, + }; +}); + +describe('RoleSelector', () => { + it('allows roles to be selected, removing any previously selected role templates', () => { + const props = { + roleMapping: { + roles: [] as string[], + role_templates: [ + { + template: { source: '' }, + }, + ], + } as RoleMapping, + canUseStoredScripts: true, + canUseInlineScripts: true, + onChange: jest.fn(), + mode: 'roles', + } as RoleSelector['props']; + + const wrapper = mountWithIntl(); + (wrapper.find(EuiComboBox).props() as any).onChange([{ label: 'foo_role' }]); + + expect(props.onChange).toHaveBeenCalledWith({ + roles: ['foo_role'], + role_templates: [], + }); + }); + + it('allows role templates to be created, removing any previously selected roles', () => { + const props = { + roleMapping: { + roles: ['foo_role'], + role_templates: [] as any, + } as RoleMapping, + canUseStoredScripts: true, + canUseInlineScripts: true, + onChange: jest.fn(), + mode: 'templates', + } as RoleSelector['props']; + + const wrapper = mountWithIntl(); + + wrapper.find(AddRoleTemplateButton).simulate('click'); + + expect(props.onChange).toHaveBeenCalledWith({ + roles: [], + role_templates: [ + { + template: { source: '' }, + }, + ], + }); + }); + + it('allows role templates to be edited', () => { + const props = { + roleMapping: { + roles: [] as string[], + role_templates: [ + { + template: { source: 'foo_role' }, + }, + ], + } as RoleMapping, + canUseStoredScripts: true, + canUseInlineScripts: true, + onChange: jest.fn(), + mode: 'templates', + } as RoleSelector['props']; + + const wrapper = mountWithIntl(); + + wrapper + .find(RoleTemplateEditor) + .props() + .onChange({ + template: { source: '{{username}}_role' }, + }); + + expect(props.onChange).toHaveBeenCalledWith({ + roles: [], + role_templates: [ + { + template: { source: '{{username}}_role' }, + }, + ], + }); + }); + + it('allows role templates to be deleted', () => { + const props = { + roleMapping: { + roles: [] as string[], + role_templates: [ + { + template: { source: 'foo_role' }, + }, + ], + } as RoleMapping, + canUseStoredScripts: true, + canUseInlineScripts: true, + onChange: jest.fn(), + mode: 'templates', + } as RoleSelector['props']; + + const wrapper = mountWithIntl(); + + findTestSubject(wrapper, 'deleteRoleTemplateButton').simulate('click'); + + expect(props.onChange).toHaveBeenCalledWith({ + roles: [], + role_templates: [], + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx new file mode 100644 index 0000000000000..6b92d6b4907f1 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; +import { RoleMapping, Role } from '../../../../../../../common/model'; +import { RolesApi } from '../../../../../../lib/roles_api'; +import { AddRoleTemplateButton } from './add_role_template_button'; +import { RoleTemplateEditor } from './role_template_editor'; + +interface Props { + roleMapping: RoleMapping; + canUseInlineScripts: boolean; + canUseStoredScripts: boolean; + mode: 'roles' | 'templates'; + onChange: (roleMapping: RoleMapping) => void; +} + +interface State { + roles: Role[]; +} + +export class RoleSelector extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { roles: [] }; + } + + public async componentDidMount() { + const roles = await RolesApi.getRoles(); + this.setState({ roles }); + } + + public render() { + const { mode } = this.props; + return ( + + {mode === 'roles' ? this.getRoleComboBox() : this.getRoleTemplates()} + + ); + } + + private getRoleComboBox = () => { + const { roles = [] } = this.props.roleMapping; + return ( + ({ label: r.name }))} + selectedOptions={roles!.map(r => ({ label: r }))} + onChange={selectedOptions => { + this.props.onChange({ + ...this.props.roleMapping, + roles: selectedOptions.map(so => so.label), + role_templates: [], + }); + }} + /> + ); + }; + + private getRoleTemplates = () => { + const { role_templates: roleTemplates = [] } = this.props.roleMapping; + return ( +
+ {roleTemplates.map((rt, index) => ( + + { + const templates = [...(this.props.roleMapping.role_templates || [])]; + templates.splice(index, 1, updatedTemplate); + this.props.onChange({ + ...this.props.roleMapping, + role_templates: templates, + }); + }} + onDelete={() => { + const templates = [...(this.props.roleMapping.role_templates || [])]; + templates.splice(index, 1); + this.props.onChange({ + ...this.props.roleMapping, + role_templates: templates, + }); + }} + /> + + + ))} + { + switch (type) { + case 'inline': { + const templates = this.props.roleMapping.role_templates || []; + this.props.onChange({ + ...this.props.roleMapping, + roles: [], + role_templates: [...templates, { template: { source: '' } }], + }); + break; + } + case 'stored': { + const templates = this.props.roleMapping.role_templates || []; + this.props.onChange({ + ...this.props.roleMapping, + roles: [], + role_templates: [...templates, { template: { id: '' } }], + }); + break; + } + default: + throw new Error(`Unsupported template type: ${type}`); + } + }} + /> +
+ ); + }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx new file mode 100644 index 0000000000000..6d4af97e12def --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { RoleTemplateEditor } from './role_template_editor'; +import { findTestSubject } from 'test_utils/find_test_subject'; + +describe('RoleTemplateEditor', () => { + it('allows inline templates to be edited', () => { + const props = { + roleTemplate: { + template: { + source: '{{username}}_foo', + }, + }, + onChange: jest.fn(), + onDelete: jest.fn(), + canUseStoredScripts: true, + canUseInlineScripts: true, + }; + + const wrapper = mountWithIntl(); + (wrapper + .find('EuiFieldText[data-test-subj="roleTemplateSourceEditor"]') + .props() as any).onChange({ target: { value: 'new_script' } }); + + expect(props.onChange).toHaveBeenCalledWith({ + template: { + source: 'new_script', + }, + }); + }); + + it('warns when editing inline scripts when they are disabled', () => { + const props = { + roleTemplate: { + template: { + source: '{{username}}_foo', + }, + }, + onChange: jest.fn(), + onDelete: jest.fn(), + canUseStoredScripts: true, + canUseInlineScripts: false, + }; + + const wrapper = mountWithIntl(); + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingInvalidRoleTemplate')).toHaveLength(0); + }); + + it('warns when editing stored scripts when they are disabled', () => { + const props = { + roleTemplate: { + template: { + id: '{{username}}_foo', + }, + }, + onChange: jest.fn(), + onDelete: jest.fn(), + canUseStoredScripts: false, + canUseInlineScripts: true, + }; + + const wrapper = mountWithIntl(); + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingInvalidRoleTemplate')).toHaveLength(0); + }); + + it('allows template types to be changed', () => { + const props = { + roleTemplate: { + template: { + source: '{{username}}_foo', + }, + }, + onChange: jest.fn(), + onDelete: jest.fn(), + canUseStoredScripts: true, + canUseInlineScripts: true, + }; + + const wrapper = mountWithIntl(); + (wrapper + .find('EuiComboBox[data-test-subj="roleMappingsFormTemplateType"]') + .props() as any).onChange('stored'); + + expect(props.onChange).toHaveBeenCalledWith({ + template: { + id: '', + }, + }); + }); + + it('warns when an invalid role template is specified', () => { + const props = { + roleTemplate: { + template: `This is a string instead of an object if the template was stored in an unparsable format in ES`, + }, + onChange: jest.fn(), + onDelete: jest.fn(), + canUseStoredScripts: true, + canUseInlineScripts: true, + }; + + const wrapper = mountWithIntl(); + expect(findTestSubject(wrapper, 'roleMappingInvalidRoleTemplate')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleTemplateSourceEditor')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleTemplateScriptIdEditor')).toHaveLength(0); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx new file mode 100644 index 0000000000000..4b8d34d271996 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiCallOut, + EuiText, + EuiSwitch, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { RoleTemplate } from '../../../../../../../common/model'; +import { + isInlineRoleTemplate, + isStoredRoleTemplate, + isInvalidRoleTemplate, +} from '../../services/role_template_type'; +import { RoleTemplateTypeSelect } from './role_template_type_select'; + +interface Props { + roleTemplate: RoleTemplate; + canUseInlineScripts: boolean; + canUseStoredScripts: boolean; + onChange: (roleTemplate: RoleTemplate) => void; + onDelete: (roleTemplate: RoleTemplate) => void; +} + +export const RoleTemplateEditor = ({ + roleTemplate, + onChange, + onDelete, + canUseInlineScripts, + canUseStoredScripts, +}: Props) => { + return ( + + {getTemplateConfigurationFields()} + {getEditorForTemplate()} + + + + + onDelete(roleTemplate)} + data-test-subj="deleteRoleTemplateButton" + > + + + + + + + ); + + function getTemplateFormatSwitch() { + const returnsJsonLabel = i18n.translate( + 'xpack.security.management.editRoleMapping.roleTemplateReturnsJson', + { + defaultMessage: 'Returns JSON', + } + ); + + return ( + + { + onChange({ + ...roleTemplate, + format: e.target.checked ? 'json' : 'string', + }); + }} + /> + + ); + } + + function getTemplateConfigurationFields() { + const templateTypeComboBox = ( + + + } + > + + + + ); + + const templateFormatSwitch = {getTemplateFormatSwitch()}; + + return ( + + + {templateTypeComboBox} + {templateFormatSwitch} + + + ); + } + + function getEditorForTemplate() { + if (isInlineRoleTemplate(roleTemplate)) { + const extraProps: Record = {}; + if (!canUseInlineScripts) { + extraProps.isInvalid = true; + extraProps.error = ( + + + + ); + } + const example = '{{username}}_role'; + return ( + + + + } + helpText={ + + } + {...extraProps} + > + { + onChange({ + ...roleTemplate, + template: { + source: e.target.value, + }, + }); + }} + /> + + + + ); + } + + if (isStoredRoleTemplate(roleTemplate)) { + const extraProps: Record = {}; + if (!canUseStoredScripts) { + extraProps.isInvalid = true; + extraProps.error = ( + + + + ); + } + return ( + + + + } + helpText={ + + } + {...extraProps} + > + { + onChange({ + ...roleTemplate, + template: { + id: e.target.value, + }, + }); + }} + /> + + + + ); + } + + if (isInvalidRoleTemplate(roleTemplate)) { + return ( + + + } + > + + + + ); + } + + throw new Error(`Unable to determine role template type`); + } +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx new file mode 100644 index 0000000000000..4a06af0fb436b --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.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 from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox } from '@elastic/eui'; +import { RoleTemplate } from '../../../../../../../common/model'; +import { isInlineRoleTemplate, isStoredRoleTemplate } from '../../services/role_template_type'; + +const templateTypeOptions = [ + { + id: 'inline', + label: i18n.translate( + 'xpack.security.management.editRoleMapping.roleTemplate.inlineTypeLabel', + { defaultMessage: 'Role template' } + ), + }, + { + id: 'stored', + label: i18n.translate( + 'xpack.security.management.editRoleMapping.roleTemplate.storedTypeLabel', + { defaultMessage: 'Stored script' } + ), + }, +]; + +interface Props { + roleTemplate: RoleTemplate; + onChange: (roleTempplate: RoleTemplate) => void; + canUseStoredScripts: boolean; + canUseInlineScripts: boolean; +} + +export const RoleTemplateTypeSelect = (props: Props) => { + const availableOptions = templateTypeOptions.filter( + ({ id }) => + (id === 'inline' && props.canUseInlineScripts) || + (id === 'stored' && props.canUseStoredScripts) + ); + + const selectedOptions = templateTypeOptions.filter( + ({ id }) => + (id === 'inline' && isInlineRoleTemplate(props.roleTemplate)) || + (id === 'stored' && isStoredRoleTemplate(props.roleTemplate)) + ); + + return ( + { + const [{ id }] = selected; + if (id === 'inline') { + props.onChange({ + ...props.roleTemplate, + template: { + source: '', + }, + }); + } else { + props.onChange({ + ...props.roleTemplate, + template: { + id: '', + }, + }); + } + }} + isClearable={false} + /> + ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss new file mode 100644 index 0000000000000..de64b80599720 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss @@ -0,0 +1,7 @@ +.secRoleMapping__ruleEditorGroup--even { + background-color: $euiColorLightestShade; +} + +.secRoleMapping__ruleEditorGroup--odd { + background-color: $euiColorEmptyShade; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx new file mode 100644 index 0000000000000..917b822acef3f --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.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 { AddRuleButton } from './add_rule_button'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { FieldRule, AllRule } from '../../../model'; + +describe('AddRuleButton', () => { + it('allows a field rule to be created', () => { + const props = { + onClick: jest.fn(), + }; + + const wrapper = mountWithIntl(); + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + expect(findTestSubject(wrapper, 'addRuleContextMenu')).toHaveLength(1); + + // EUI renders this ID twice, so we need to target the button itself + wrapper.find('button[id="addRuleOption"]').simulate('click'); + + expect(props.onClick).toHaveBeenCalledTimes(1); + + const [newRule] = props.onClick.mock.calls[0]; + expect(newRule).toBeInstanceOf(FieldRule); + expect(newRule.toRaw()).toEqual({ + field: { username: '*' }, + }); + }); + + it('allows a rule group to be created', () => { + const props = { + onClick: jest.fn(), + }; + + const wrapper = mountWithIntl(); + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + expect(findTestSubject(wrapper, 'addRuleContextMenu')).toHaveLength(1); + + // EUI renders this ID twice, so we need to target the button itself + wrapper.find('button[id="addRuleGroupOption"]').simulate('click'); + + expect(props.onClick).toHaveBeenCalledTimes(1); + + const [newRule] = props.onClick.mock.calls[0]; + expect(newRule).toBeInstanceOf(AllRule); + expect(newRule.toRaw()).toEqual({ + all: [{ field: { username: '*' } }], + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx new file mode 100644 index 0000000000000..100c0dd3eeaee --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Rule, FieldRule, AllRule } from '../../../model'; + +interface Props { + onClick: (newRule: Rule) => void; +} + +export const AddRuleButton = (props: Props) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const button = ( + { + setIsMenuOpen(!isMenuOpen); + }} + > + + + ); + + const options = [ + { + setIsMenuOpen(false); + props.onClick(new FieldRule('username', '*')); + }} + > + + , + { + setIsMenuOpen(false); + props.onClick(new AllRule([new FieldRule('username', '*')])); + }} + > + + , + ]; + + return ( + setIsMenuOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="downLeft" + > + + + ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx new file mode 100644 index 0000000000000..8d5d5c99ee99d --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { FieldRuleEditor } from './field_rule_editor'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { FieldRule } from '../../../model'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { ReactWrapper } from 'enzyme'; + +function assertField(wrapper: ReactWrapper, index: number, field: string) { + const isFirst = index === 0; + if (isFirst) { + expect( + wrapper.find(`EuiComboBox[data-test-subj~="fieldRuleEditorField-${index}"]`).props() + ).toMatchObject({ + selectedOptions: [{ label: field }], + }); + + expect(findTestSubject(wrapper, `fieldRuleEditorField-${index}-combo`)).toHaveLength(1); + expect(findTestSubject(wrapper, `fieldRuleEditorField-${index}-expression`)).toHaveLength(0); + } else { + expect( + wrapper.find(`EuiExpression[data-test-subj~="fieldRuleEditorField-${index}"]`).props() + ).toMatchObject({ + value: field, + }); + + expect(findTestSubject(wrapper, `fieldRuleEditorField-${index}-combo`)).toHaveLength(0); + expect(findTestSubject(wrapper, `fieldRuleEditorField-${index}-expression`)).toHaveLength(1); + } +} + +function assertValueType(wrapper: ReactWrapper, index: number, type: string) { + const valueTypeField = findTestSubject(wrapper, `fieldRuleEditorValueType-${index}`); + expect(valueTypeField.props()).toMatchObject({ value: type }); +} + +function assertValue(wrapper: ReactWrapper, index: number, value: any) { + const valueField = findTestSubject(wrapper, `fieldRuleEditorValue-${index}`); + expect(valueField.props()).toMatchObject({ value }); +} + +describe('FieldRuleEditor', () => { + it('can render a text-based field rule', () => { + const props = { + rule: new FieldRule('username', '*'), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + assertField(wrapper, 0, 'username'); + assertValueType(wrapper, 0, 'text'); + assertValue(wrapper, 0, '*'); + }); + + it('can render a number-based field rule', () => { + const props = { + rule: new FieldRule('username', 12), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + assertField(wrapper, 0, 'username'); + assertValueType(wrapper, 0, 'number'); + assertValue(wrapper, 0, 12); + }); + + it('can render a null-based field rule', () => { + const props = { + rule: new FieldRule('username', null), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + assertField(wrapper, 0, 'username'); + assertValueType(wrapper, 0, 'null'); + assertValue(wrapper, 0, '-- null --'); + }); + + it('can render a boolean-based field rule (true)', () => { + const props = { + rule: new FieldRule('username', true), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + assertField(wrapper, 0, 'username'); + assertValueType(wrapper, 0, 'boolean'); + assertValue(wrapper, 0, 'true'); + }); + + it('can render a boolean-based field rule (false)', () => { + const props = { + rule: new FieldRule('username', false), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + assertField(wrapper, 0, 'username'); + assertValueType(wrapper, 0, 'boolean'); + assertValue(wrapper, 0, 'false'); + }); + + it('can render with alternate values specified', () => { + const props = { + rule: new FieldRule('username', ['*', 12, null, true, false]), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + expect(findTestSubject(wrapper, 'addAlternateValueButton')).toHaveLength(1); + + assertField(wrapper, 0, 'username'); + assertValueType(wrapper, 0, 'text'); + assertValue(wrapper, 0, '*'); + + assertField(wrapper, 1, 'username'); + assertValueType(wrapper, 1, 'number'); + assertValue(wrapper, 1, 12); + + assertField(wrapper, 2, 'username'); + assertValueType(wrapper, 2, 'null'); + assertValue(wrapper, 2, '-- null --'); + + assertField(wrapper, 3, 'username'); + assertValueType(wrapper, 3, 'boolean'); + assertValue(wrapper, 3, 'true'); + + assertField(wrapper, 4, 'username'); + assertValueType(wrapper, 4, 'boolean'); + assertValue(wrapper, 4, 'false'); + }); + + it('allows alternate values to be added when "allowAdd" is set to true', () => { + const props = { + rule: new FieldRule('username', null), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + findTestSubject(wrapper, 'addAlternateValueButton').simulate('click'); + expect(props.onChange).toHaveBeenCalledTimes(1); + const [updatedRule] = props.onChange.mock.calls[0]; + expect(updatedRule.toRaw()).toEqual({ + field: { + username: [null, '*'], + }, + }); + }); + + it('allows values to be deleted; deleting all values invokes "onDelete"', () => { + const props = { + rule: new FieldRule('username', ['*', 12, null]), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + + expect(findTestSubject(wrapper, `fieldRuleEditorDeleteValue`)).toHaveLength(3); + findTestSubject(wrapper, `fieldRuleEditorDeleteValue-0`).simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [updatedRule1] = props.onChange.mock.calls[0]; + expect(updatedRule1.toRaw()).toEqual({ + field: { + username: [12, null], + }, + }); + + props.onChange.mockReset(); + + // simulate updated rule being fed back in + wrapper.setProps({ rule: updatedRule1 }); + + expect(findTestSubject(wrapper, `fieldRuleEditorDeleteValue`)).toHaveLength(2); + findTestSubject(wrapper, `fieldRuleEditorDeleteValue-1`).simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [updatedRule2] = props.onChange.mock.calls[0]; + expect(updatedRule2.toRaw()).toEqual({ + field: { + username: [12], + }, + }); + + props.onChange.mockReset(); + + // simulate updated rule being fed back in + wrapper.setProps({ rule: updatedRule2 }); + + expect(findTestSubject(wrapper, `fieldRuleEditorDeleteValue`)).toHaveLength(1); + findTestSubject(wrapper, `fieldRuleEditorDeleteValue-0`).simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(0); + expect(props.onDelete).toHaveBeenCalledTimes(1); + }); + + it('allows field data types to be changed', () => { + const props = { + rule: new FieldRule('username', '*'), + onChange: jest.fn(), + onDelete: jest.fn(), + }; + + const wrapper = mountWithIntl(); + + const { onChange } = findTestSubject(wrapper, `fieldRuleEditorValueType-0`).props(); + onChange!({ target: { value: 'number' } as any } as any); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [updatedRule] = props.onChange.mock.calls[0]; + expect(updatedRule.toRaw()).toEqual({ + field: { + username: 0, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx new file mode 100644 index 0000000000000..52cf70dbd12bd --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx @@ -0,0 +1,380 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, ChangeEvent } from 'react'; +import { + EuiButtonIcon, + EuiExpression, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiComboBox, + EuiSelect, + EuiFieldNumber, + EuiIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FieldRule, FieldRuleValue } from '../../../model'; + +interface Props { + rule: FieldRule; + onChange: (rule: FieldRule) => void; + onDelete: () => void; +} + +const userFields = [ + { + name: 'username', + }, + { + name: 'dn', + }, + { + name: 'groups', + }, + { + name: 'realm', + }, +]; + +const fieldOptions = userFields.map(f => ({ label: f.name })); + +type ComparisonOption = 'text' | 'number' | 'null' | 'boolean'; +const comparisonOptions: Record< + ComparisonOption, + { id: ComparisonOption; defaultValue: FieldRuleValue } +> = { + text: { + id: 'text', + defaultValue: '*', + }, + number: { + id: 'number', + defaultValue: 0, + }, + null: { + id: 'null', + defaultValue: null, + }, + boolean: { + id: 'boolean', + defaultValue: true, + }, +}; + +export class FieldRuleEditor extends Component { + public render() { + const { field, value } = this.props.rule; + + const content = Array.isArray(value) + ? value.map((v, index) => this.renderFieldRow(field, value, index)) + : [this.renderFieldRow(field, value, 0)]; + + return ( + + {content.map((row, index) => { + return {row}; + })} + + ); + } + + private renderFieldRow = (field: string, ruleValue: FieldRuleValue, valueIndex: number) => { + const isPrimaryRow = valueIndex === 0; + + let renderAddValueButton = true; + let rowRuleValue: FieldRuleValue = ruleValue; + if (Array.isArray(ruleValue)) { + renderAddValueButton = ruleValue.length - 1 === valueIndex; + rowRuleValue = ruleValue[valueIndex]; + } + + const comparisonType = this.getComparisonType(rowRuleValue); + + return ( + + + {isPrimaryRow ? ( + + + + ) : ( + + + + )} + + + {this.renderFieldTypeInput(comparisonType.id, valueIndex)} + + + {this.renderFieldValueInput(comparisonType.id, rowRuleValue, valueIndex)} + + + + {renderAddValueButton ? ( + + ) : ( + + )} + + + + + this.onRemoveAlternateValue(valueIndex)} + /> + + + + ); + }; + + private renderFieldTypeInput = (inputType: ComparisonOption, valueIndex: number) => { + return ( + + + this.onComparisonTypeChange(valueIndex, e.target.value as ComparisonOption) + } + /> + + ); + }; + + private renderFieldValueInput = ( + fieldType: ComparisonOption, + rowRuleValue: FieldRuleValue, + valueIndex: number + ) => { + const inputField = this.getInputFieldForType(fieldType, rowRuleValue, valueIndex); + + return ( + + {inputField} + + ); + }; + + private getInputFieldForType = ( + fieldType: ComparisonOption, + rowRuleValue: FieldRuleValue, + valueIndex: number + ) => { + const isNullValue = rowRuleValue === null; + + const commonProps = { + 'data-test-subj': `fieldRuleEditorValue-${valueIndex}`, + }; + + switch (fieldType) { + case 'boolean': + return ( + + ); + case 'text': + case 'null': + return ( + + ); + case 'number': + return ( + + ); + default: + throw new Error(`Unsupported input field type: ${fieldType}`); + } + }; + + private onAddAlternateValue = () => { + const { field, value } = this.props.rule; + const nextValue = Array.isArray(value) ? [...value] : [value]; + nextValue.push('*'); + this.props.onChange(new FieldRule(field, nextValue)); + }; + + private onRemoveAlternateValue = (index: number) => { + const { field, value } = this.props.rule; + + if (!Array.isArray(value) || value.length === 1) { + // Only one value left. Delete entire rule instead. + this.props.onDelete(); + return; + } + const nextValue = [...value]; + nextValue.splice(index, 1); + this.props.onChange(new FieldRule(field, nextValue)); + }; + + private onFieldChange = ([newField]: Array<{ label: string }>) => { + if (!newField) { + return; + } + + const { value } = this.props.rule; + this.props.onChange(new FieldRule(newField.label, value)); + }; + + private onAddField = (newField: string) => { + const { value } = this.props.rule; + this.props.onChange(new FieldRule(newField, value)); + }; + + private onValueChange = (index: number) => (e: ChangeEvent) => { + const { field, value } = this.props.rule; + let nextValue; + if (Array.isArray(value)) { + nextValue = [...value]; + nextValue.splice(index, 1, e.target.value); + } else { + nextValue = e.target.value; + } + this.props.onChange(new FieldRule(field, nextValue)); + }; + + private onNumericValueChange = (index: number) => (e: ChangeEvent) => { + const { field, value } = this.props.rule; + let nextValue; + if (Array.isArray(value)) { + nextValue = [...value]; + nextValue.splice(index, 1, parseFloat(e.target.value)); + } else { + nextValue = parseFloat(e.target.value); + } + this.props.onChange(new FieldRule(field, nextValue)); + }; + + private onBooleanValueChange = (index: number) => (e: ChangeEvent) => { + const boolValue = e.target.value === 'true'; + + const { field, value } = this.props.rule; + let nextValue; + if (Array.isArray(value)) { + nextValue = [...value]; + nextValue.splice(index, 1, boolValue); + } else { + nextValue = boolValue; + } + this.props.onChange(new FieldRule(field, nextValue)); + }; + + private onComparisonTypeChange = (index: number, newType: ComparisonOption) => { + const comparison = comparisonOptions[newType]; + if (!comparison) { + throw new Error(`Unexpected comparison type: ${newType}`); + } + const { field, value } = this.props.rule; + let nextValue = value; + if (Array.isArray(value)) { + nextValue = [...value]; + nextValue.splice(index, 1, comparison.defaultValue as any); + } else { + nextValue = comparison.defaultValue; + } + this.props.onChange(new FieldRule(field, nextValue)); + }; + + private getComparisonType(ruleValue: FieldRuleValue) { + const valueType = typeof ruleValue; + if (valueType === 'string' || valueType === 'undefined') { + return comparisonOptions.text; + } + if (valueType === 'number') { + return comparisonOptions.number; + } + if (valueType === 'boolean') { + return comparisonOptions.boolean; + } + if (ruleValue === null) { + return comparisonOptions.null; + } + throw new Error(`Unable to detect comparison type for rule value [${ruleValue}]`); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx new file mode 100644 index 0000000000000..dc09cb1e591fa --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuleEditorPanel } from './rule_editor_panel'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx new file mode 100644 index 0000000000000..8a9b37ab0f406 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import 'brace'; +import 'brace/mode/json'; + +// brace/ace uses the Worker class, which is not currently provided by JSDOM. +// This is not required for the tests to pass, but it rather suppresses lengthy +// warnings in the console which adds unnecessary noise to the test output. +import 'test_utils/stub_web_worker'; + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { JSONRuleEditor } from './json_rule_editor'; +import { EuiCodeEditor } from '@elastic/eui'; +import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; + +describe('JSONRuleEditor', () => { + it('renders an empty rule set', () => { + const props = { + rules: null, + onChange: jest.fn(), + onValidityChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + expect(props.onChange).not.toHaveBeenCalled(); + expect(props.onValidityChange).not.toHaveBeenCalled(); + + expect(wrapper.find(EuiCodeEditor).props().value).toMatchInlineSnapshot(`"{}"`); + }); + + it('renders a rule set', () => { + const props = { + rules: new AllRule([ + new AnyRule([new FieldRule('username', '*')]), + new ExceptAnyRule([ + new FieldRule('metadata.foo.bar', '*'), + new AllRule([new FieldRule('realm', 'special-one')]), + ]), + new ExceptAllRule([new FieldRule('realm', '*')]), + ]), + onChange: jest.fn(), + onValidityChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + const { value } = wrapper.find(EuiCodeEditor).props(); + expect(JSON.parse(value)).toEqual({ + all: [ + { + any: [{ field: { username: '*' } }], + }, + { + except: { + any: [ + { field: { 'metadata.foo.bar': '*' } }, + { + all: [{ field: { realm: 'special-one' } }], + }, + ], + }, + }, + { + except: { + all: [{ field: { realm: '*' } }], + }, + }, + ], + }); + }); + + it('notifies when input contains invalid JSON', () => { + const props = { + rules: null, + onChange: jest.fn(), + onValidityChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + const allRule = JSON.stringify(new AllRule().toRaw()); + act(() => { + wrapper + .find(EuiCodeEditor) + .props() + .onChange(allRule + ', this makes invalid JSON'); + }); + + expect(props.onValidityChange).toHaveBeenCalledTimes(1); + expect(props.onValidityChange).toHaveBeenCalledWith(false); + expect(props.onChange).not.toHaveBeenCalled(); + }); + + it('notifies when input contains an invalid rule set, even if it is valid JSON', () => { + const props = { + rules: null, + onChange: jest.fn(), + onValidityChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + const invalidRule = JSON.stringify({ + all: [ + { + field: { + foo: {}, + }, + }, + ], + }); + + act(() => { + wrapper + .find(EuiCodeEditor) + .props() + .onChange(invalidRule); + }); + + expect(props.onValidityChange).toHaveBeenCalledTimes(1); + expect(props.onValidityChange).toHaveBeenCalledWith(false); + expect(props.onChange).not.toHaveBeenCalled(); + }); + + it('fires onChange when a valid rule set is provided after being previously invalidated', () => { + const props = { + rules: null, + onChange: jest.fn(), + onValidityChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + const allRule = JSON.stringify(new AllRule().toRaw()); + act(() => { + wrapper + .find(EuiCodeEditor) + .props() + .onChange(allRule + ', this makes invalid JSON'); + }); + + expect(props.onValidityChange).toHaveBeenCalledTimes(1); + expect(props.onValidityChange).toHaveBeenCalledWith(false); + expect(props.onChange).not.toHaveBeenCalled(); + + props.onValidityChange.mockReset(); + + act(() => { + wrapper + .find(EuiCodeEditor) + .props() + .onChange(allRule); + }); + + expect(props.onValidityChange).toHaveBeenCalledTimes(1); + expect(props.onValidityChange).toHaveBeenCalledWith(true); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [updatedRule] = props.onChange.mock.calls[0]; + expect(JSON.stringify(updatedRule.toRaw())).toEqual(allRule); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx new file mode 100644 index 0000000000000..371fb59f7a5d1 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, Fragment } from 'react'; + +import 'brace/mode/json'; +import 'brace/theme/github'; +import { EuiCodeEditor, EuiFormRow, EuiButton, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../../model'; +import { documentationLinks } from '../../../services/documentation_links'; + +interface Props { + rules: Rule | null; + onChange: (updatedRules: Rule | null) => void; + onValidityChange: (isValid: boolean) => void; +} + +export const JSONRuleEditor = (props: Props) => { + const [rawRules, setRawRules] = useState( + JSON.stringify(props.rules ? props.rules.toRaw() : {}, null, 2) + ); + + const [ruleBuilderError, setRuleBuilderError] = useState(null); + + function onRulesChange(updatedRules: string) { + setRawRules(updatedRules); + // Fire onChange only if rules are valid + try { + const ruleJSON = JSON.parse(updatedRules); + props.onChange(generateRulesFromRaw(ruleJSON).rules); + props.onValidityChange(true); + setRuleBuilderError(null); + } catch (e) { + if (e instanceof RuleBuilderError) { + setRuleBuilderError(e); + } else { + setRuleBuilderError(null); + } + props.onValidityChange(false); + } + } + + function reformatRules() { + try { + const ruleJSON = JSON.parse(rawRules); + setRawRules(JSON.stringify(ruleJSON, null, 2)); + } catch (ignore) { + // ignore + } + } + + return ( + + + + + + + + + +

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

+
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx new file mode 100644 index 0000000000000..809264183d30c --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { RuleEditorPanel } from '.'; +import { VisualRuleEditor } from './visual_rule_editor'; +import { JSONRuleEditor } from './json_rule_editor'; +import { findTestSubject } from 'test_utils/find_test_subject'; + +// brace/ace uses the Worker class, which is not currently provided by JSDOM. +// This is not required for the tests to pass, but it rather suppresses lengthy +// warnings in the console which adds unnecessary noise to the test output. +import 'test_utils/stub_web_worker'; +import { AllRule, FieldRule } from '../../../model'; +import { EuiErrorBoundary } from '@elastic/eui'; + +describe('RuleEditorPanel', () => { + it('renders the visual editor when no rules are defined', () => { + const props = { + rawRules: {}, + onChange: jest.fn(), + onValidityChange: jest.fn(), + validateForm: false, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + }); + + it('allows switching to the JSON editor, carrying over rules', () => { + const props = { + rawRules: { + all: [ + { + field: { + username: ['*'], + }, + }, + ], + }, + onChange: jest.fn(), + onValidityChange: jest.fn(), + validateForm: false, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + + findTestSubject(wrapper, 'roleMappingsJSONRuleEditorButton').simulate('click'); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + + const jsonEditor = wrapper.find(JSONRuleEditor); + expect(jsonEditor).toHaveLength(1); + const { rules } = jsonEditor.props(); + expect(rules!.toRaw()).toEqual(props.rawRules); + }); + + it('allows switching to the visual editor, carrying over rules', () => { + const props = { + rawRules: { + field: { username: '*' }, + }, + onChange: jest.fn(), + onValidityChange: jest.fn(), + validateForm: false, + }; + const wrapper = mountWithIntl(); + + findTestSubject(wrapper, 'roleMappingsJSONRuleEditorButton').simulate('click'); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); + + const jsonEditor = wrapper.find(JSONRuleEditor); + expect(jsonEditor).toHaveLength(1); + const { rules: initialRules, onChange } = jsonEditor.props(); + expect(initialRules?.toRaw()).toEqual({ + field: { username: '*' }, + }); + + onChange(new AllRule([new FieldRule('otherRule', 12)])); + + findTestSubject(wrapper, 'roleMappingsVisualRuleEditorButton').simulate('click'); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [rules] = props.onChange.mock.calls[0]; + expect(rules).toEqual({ + all: [{ field: { otherRule: 12 } }], + }); + }); + + it('catches errors thrown by child components', () => { + const props = { + rawRules: {}, + onChange: jest.fn(), + onValidityChange: jest.fn(), + validateForm: false, + }; + const wrapper = mountWithIntl(); + + wrapper.find(VisualRuleEditor).simulateError(new Error('Something awful happened here.')); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(EuiErrorBoundary)).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx new file mode 100644 index 0000000000000..4aab49b2b2efc --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx @@ -0,0 +1,298 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { + EuiSpacer, + EuiConfirmModal, + EuiOverlayMask, + EuiCallOut, + EuiErrorBoundary, + EuiIcon, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiFormRow, + EuiPanel, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { RoleMapping } from '../../../../../../../common/model'; +import { VisualRuleEditor } from './visual_rule_editor'; +import { JSONRuleEditor } from './json_rule_editor'; +import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; +import { Rule, generateRulesFromRaw } from '../../../model'; +import { validateRoleMappingRules } from '../../services/role_mapping_validation'; +import { documentationLinks } from '../../../services/documentation_links'; + +interface Props { + rawRules: RoleMapping['rules']; + onChange: (rawRules: RoleMapping['rules']) => void; + onValidityChange: (isValid: boolean) => void; + validateForm: boolean; +} + +interface State { + rules: Rule | null; + maxDepth: number; + isRuleValid: boolean; + showConfirmModeChange: boolean; + showVisualEditorDisabledAlert: boolean; + mode: 'visual' | 'json'; +} + +export class RuleEditorPanel extends Component { + constructor(props: Props) { + super(props); + this.state = { + ...this.initializeFromRawRules(props.rawRules), + isRuleValid: true, + showConfirmModeChange: false, + showVisualEditorDisabledAlert: false, + }; + } + + public render() { + const validationResult = + this.props.validateForm && + validateRoleMappingRules({ rules: this.state.rules ? this.state.rules.toRaw() : {} }); + + let validationWarning = null; + if (validationResult && validationResult.error) { + validationWarning = ( + + + + ); + } + + return ( + + +

+ +

+
+ + + +

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

+
+
+ + + + + {validationWarning} + {this.getEditor()} + + {this.getModeToggle()} + {this.getConfirmModeChangePrompt()} + + + + +
+
+ ); + } + + private initializeFromRawRules = (rawRules: Props['rawRules']) => { + const { rules, maxDepth } = generateRulesFromRaw(rawRules); + const mode: State['mode'] = maxDepth >= VISUAL_MAX_RULE_DEPTH ? 'json' : 'visual'; + return { + rules, + mode, + maxDepth, + }; + }; + + private getModeToggle() { + if (this.state.mode === 'json' && this.state.maxDepth > VISUAL_MAX_RULE_DEPTH) { + return ( + + + + ); + } + + // Don't offer swith if no rules are present yet + if (this.state.mode === 'visual' && this.state.rules === null) { + return null; + } + + switch (this.state.mode) { + case 'visual': + return ( + { + this.trySwitchEditorMode('json'); + }} + > + + {' '} + + + + ); + case 'json': + return ( + { + this.trySwitchEditorMode('visual'); + }} + > + + {' '} + + + + ); + default: + throw new Error(`Unexpected rule editor mode: ${this.state.mode}`); + } + } + + private getEditor() { + switch (this.state.mode) { + case 'visual': + return ( + this.trySwitchEditorMode('json')} + /> + ); + case 'json': + return ( + + ); + default: + throw new Error(`Unexpected rule editor mode: ${this.state.mode}`); + } + } + + private getConfirmModeChangePrompt = () => { + if (!this.state.showConfirmModeChange) { + return null; + } + return ( + + + } + onCancel={() => this.setState({ showConfirmModeChange: false })} + onConfirm={() => { + this.setState({ mode: 'visual', showConfirmModeChange: false }); + this.onValidityChange(true); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

+ +

+
+
+ ); + }; + + private onRuleChange = (updatedRule: Rule | null) => { + const raw = updatedRule ? updatedRule.toRaw() : {}; + this.props.onChange(raw); + this.setState({ + ...generateRulesFromRaw(raw), + }); + }; + + private onValidityChange = (isRuleValid: boolean) => { + this.setState({ isRuleValid }); + this.props.onValidityChange(isRuleValid); + }; + + private trySwitchEditorMode = (newMode: State['mode']) => { + switch (newMode) { + case 'visual': { + if (this.state.isRuleValid) { + this.setState({ mode: newMode }); + this.onValidityChange(true); + } else { + this.setState({ showConfirmModeChange: true }); + } + break; + } + case 'json': + this.setState({ mode: newMode }); + this.onValidityChange(true); + break; + default: + throw new Error(`Unexpected rule editor mode: ${this.state.mode}`); + } + }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx new file mode 100644 index 0000000000000..3e0e0e386e98c --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { RuleGroupEditor } from './rule_group_editor'; +import { shallowWithIntl, mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../../model'; +import { FieldRuleEditor } from './field_rule_editor'; +import { AddRuleButton } from './add_rule_button'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { findTestSubject } from 'test_utils/find_test_subject'; + +describe('RuleGroupEditor', () => { + it('renders an empty group', () => { + const props = { + rule: new AllRule([]), + allowAdd: true, + ruleDepth: 0, + onChange: jest.fn(), + onDelete: jest.fn(), + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(RuleGroupEditor)).toHaveLength(0); + expect(wrapper.find(FieldRuleEditor)).toHaveLength(0); + expect(wrapper.find(AddRuleButton)).toHaveLength(1); + }); + + it('allows the group type to be changed, maintaining child rules', async () => { + const props = { + rule: new AllRule([new FieldRule('username', '*')]), + allowAdd: true, + ruleDepth: 0, + onChange: jest.fn(), + onDelete: jest.fn(), + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(RuleGroupEditor)).toHaveLength(1); + expect(wrapper.find(FieldRuleEditor)).toHaveLength(1); + expect(wrapper.find(AddRuleButton)).toHaveLength(1); + expect(findTestSubject(wrapper, 'deleteRuleGroupButton')).toHaveLength(1); + + const anyRule = new AnyRule(); + + findTestSubject(wrapper, 'ruleGroupTitle').simulate('click'); + await nextTick(); + wrapper.update(); + + const anyRuleOption = wrapper.find(EuiContextMenuItem).filterWhere(menuItem => { + return menuItem.text() === anyRule.getDisplayTitle(); + }); + + anyRuleOption.simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [newRule] = props.onChange.mock.calls[0]; + expect(newRule).toBeInstanceOf(AnyRule); + expect(newRule.toRaw()).toEqual(new AnyRule([new FieldRule('username', '*')]).toRaw()); + }); + + it('warns when changing group types which would invalidate child rules', async () => { + const props = { + rule: new AllRule([new ExceptAnyRule([new FieldRule('my_custom_field', 'foo*')])]), + allowAdd: true, + ruleDepth: 0, + onChange: jest.fn(), + onDelete: jest.fn(), + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(RuleGroupEditor)).toHaveLength(2); + expect(wrapper.find(FieldRuleEditor)).toHaveLength(1); + expect(wrapper.find(AddRuleButton)).toHaveLength(2); + expect(findTestSubject(wrapper, 'deleteRuleGroupButton')).toHaveLength(2); + + const anyRule = new AnyRule(); + + findTestSubject(wrapper, 'ruleGroupTitle') + .first() + .simulate('click'); + await nextTick(); + wrapper.update(); + + const anyRuleOption = wrapper.find(EuiContextMenuItem).filterWhere(menuItem => { + return menuItem.text() === anyRule.getDisplayTitle(); + }); + + anyRuleOption.simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(0); + expect(findTestSubject(wrapper, 'confirmRuleChangeModal')).toHaveLength(1); + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [newRule] = props.onChange.mock.calls[0]; + expect(newRule).toBeInstanceOf(AnyRule); + + // new rule should a defaulted field sub rule, as the existing rules are not valid for the new type + expect(newRule.toRaw()).toEqual(new AnyRule([new FieldRule('username', '*')]).toRaw()); + }); + + it('does not change groups when canceling the confirmation', async () => { + const props = { + rule: new AllRule([new ExceptAnyRule([new FieldRule('username', '*')])]), + allowAdd: true, + ruleDepth: 0, + onChange: jest.fn(), + onDelete: jest.fn(), + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(RuleGroupEditor)).toHaveLength(2); + expect(wrapper.find(FieldRuleEditor)).toHaveLength(1); + expect(wrapper.find(AddRuleButton)).toHaveLength(2); + expect(findTestSubject(wrapper, 'deleteRuleGroupButton')).toHaveLength(2); + + const anyRule = new AnyRule(); + + findTestSubject(wrapper, 'ruleGroupTitle') + .first() + .simulate('click'); + await nextTick(); + wrapper.update(); + + const anyRuleOption = wrapper.find(EuiContextMenuItem).filterWhere(menuItem => { + return menuItem.text() === anyRule.getDisplayTitle(); + }); + + anyRuleOption.simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(0); + expect(findTestSubject(wrapper, 'confirmRuleChangeModal')).toHaveLength(1); + findTestSubject(wrapper, 'confirmModalCancelButton').simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(0); + }); + + it('hides the add rule button when instructed to', () => { + const props = { + rule: new AllRule([]), + allowAdd: false, + ruleDepth: 0, + onChange: jest.fn(), + onDelete: jest.fn(), + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(AddRuleButton)).toHaveLength(0); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx new file mode 100644 index 0000000000000..6fb33db179e8a --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AddRuleButton } from './add_rule_button'; +import { RuleGroupTitle } from './rule_group_title'; +import { FieldRuleEditor } from './field_rule_editor'; +import { RuleGroup, Rule, FieldRule } from '../../../model'; +import { isRuleGroup } from '../../services/is_rule_group'; + +interface Props { + rule: RuleGroup; + allowAdd: boolean; + parentRule?: RuleGroup; + ruleDepth: number; + onChange: (rule: RuleGroup) => void; + onDelete: () => void; +} +export class RuleGroupEditor extends Component { + public render() { + return ( + + + + + + + + + + + + + + + {this.renderSubRules()} + {this.props.allowAdd && ( + + + + )} + + + ); + } + + private renderSubRules = () => { + return this.props.rule.getRules().map((subRule, subRuleIndex, rules) => { + const isLastRule = subRuleIndex === rules.length - 1; + const divider = isLastRule ? null : ( + + + + ); + + if (isRuleGroup(subRule)) { + return ( + + + { + const updatedRule = this.props.rule.clone() as RuleGroup; + updatedRule.replaceRule(subRuleIndex, updatedSubRule); + this.props.onChange(updatedRule); + }} + onDelete={() => { + const updatedRule = this.props.rule.clone() as RuleGroup; + updatedRule.removeRule(subRuleIndex); + this.props.onChange(updatedRule); + }} + /> + + {divider} + + ); + } + + return ( + + + { + const updatedRule = this.props.rule.clone() as RuleGroup; + updatedRule.replaceRule(subRuleIndex, updatedSubRule); + this.props.onChange(updatedRule); + }} + onDelete={() => { + const updatedRule = this.props.rule.clone() as RuleGroup; + updatedRule.removeRule(subRuleIndex); + this.props.onChange(updatedRule); + }} + /> + + {divider} + + ); + }); + }; + + private onAddRuleClick = (newRule: Rule) => { + const updatedRule = this.props.rule.clone() as RuleGroup; + updatedRule.addRule(newRule); + this.props.onChange(updatedRule); + }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx new file mode 100644 index 0000000000000..e46893afd4d86 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiLink, + EuiIcon, + EuiOverlayMask, + EuiConfirmModal, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + RuleGroup, + AllRule, + AnyRule, + ExceptAllRule, + ExceptAnyRule, + FieldRule, +} from '../../../model'; + +interface Props { + rule: RuleGroup; + readonly?: boolean; + parentRule?: RuleGroup; + onChange: (rule: RuleGroup) => void; +} + +const rules = [new AllRule(), new AnyRule()]; +const exceptRules = [new ExceptAllRule(), new ExceptAnyRule()]; + +export const RuleGroupTitle = (props: Props) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const [showConfirmChangeModal, setShowConfirmChangeModal] = useState(false); + const [pendingNewRule, setPendingNewRule] = useState(null); + + const canUseExcept = props.parentRule && props.parentRule.canContainRules(exceptRules); + + const availableRuleTypes = [...rules, ...(canUseExcept ? exceptRules : [])]; + + const onChange = (newRule: RuleGroup) => { + const currentSubRules = props.rule.getRules(); + const areSubRulesValid = newRule.canContainRules(currentSubRules); + if (areSubRulesValid) { + const clone = newRule.clone() as RuleGroup; + currentSubRules.forEach(subRule => clone.addRule(subRule)); + + props.onChange(clone); + setIsMenuOpen(false); + } else { + setPendingNewRule(newRule); + setShowConfirmChangeModal(true); + } + }; + + const changeRuleDiscardingSubRules = (newRule: RuleGroup) => { + // Ensure a default sub rule is present when not carrying over the original sub rules + const newRuleInstance = newRule.clone() as RuleGroup; + if (newRuleInstance.getRules().length === 0) { + newRuleInstance.addRule(new FieldRule('username', '*')); + } + + props.onChange(newRuleInstance); + setIsMenuOpen(false); + }; + + const ruleButton = ( + setIsMenuOpen(!isMenuOpen)} data-test-subj="ruleGroupTitle"> + {props.rule.getDisplayTitle()} + + ); + + const ruleTypeSelector = ( + setIsMenuOpen(false)}> + { + const isSelected = rt.getDisplayTitle() === props.rule.getDisplayTitle(); + const icon = isSelected ? 'check' : 'empty'; + return ( + onChange(rt as RuleGroup)}> + {rt.getDisplayTitle()} + + ); + })} + /> + + ); + + const confirmChangeModal = showConfirmChangeModal ? ( + + + } + onCancel={() => { + setShowConfirmChangeModal(false); + setPendingNewRule(null); + }} + onConfirm={() => { + setShowConfirmChangeModal(false); + changeRuleDiscardingSubRules(pendingNewRule!); + setPendingNewRule(null); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

+ +

+
+
+ ) : null; + + return ( +

+ {ruleTypeSelector} + {confirmChangeModal} +

+ ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx new file mode 100644 index 0000000000000..7c63613ee1cc9 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { VisualRuleEditor } from './visual_rule_editor'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { RuleGroupEditor } from './rule_group_editor'; +import { FieldRuleEditor } from './field_rule_editor'; + +describe('VisualRuleEditor', () => { + it('renders an empty prompt when no rules are defined', () => { + const props = { + rules: null, + maxDepth: 0, + onSwitchEditorMode: jest.fn(), + onChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + expect(props.onChange).toHaveBeenCalledTimes(1); + const [newRule] = props.onChange.mock.calls[0]; + expect(newRule.toRaw()).toEqual({ + all: [{ field: { username: '*' } }], + }); + }); + + it('adds a rule group when the "Add rules" button is clicked', () => { + const props = { + rules: null, + maxDepth: 0, + onSwitchEditorMode: jest.fn(), + onChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + expect(findTestSubject(wrapper, 'roleMappingsNoRulesDefined')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingsRulesTooComplex')).toHaveLength(0); + }); + + it('clicking the add button when no rules are defined populates an initial rule set', () => { + const props = { + rules: null, + maxDepth: 0, + onSwitchEditorMode: jest.fn(), + onChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + + expect(props.onChange).toHaveBeenCalledTimes(1); + const [newRule] = props.onChange.mock.calls[0]; + expect(newRule).toBeInstanceOf(AllRule); + expect(newRule.toRaw()).toEqual({ + all: [ + { + field: { + username: '*', + }, + }, + ], + }); + }); + + it('renders a nested rule set', () => { + const props = { + rules: new AllRule([ + new AnyRule([new FieldRule('username', '*')]), + new ExceptAnyRule([ + new FieldRule('metadata.foo.bar', '*'), + new AllRule([new FieldRule('realm', 'special-one')]), + ]), + new ExceptAllRule([new FieldRule('realm', '*')]), + ]), + maxDepth: 4, + onSwitchEditorMode: jest.fn(), + onChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + + expect(wrapper.find(RuleGroupEditor)).toHaveLength(5); + expect(wrapper.find(FieldRuleEditor)).toHaveLength(4); + expect(findTestSubject(wrapper, 'roleMappingsRulesTooComplex')).toHaveLength(0); + }); + + it('warns when the rule set is too complex', () => { + const props = { + rules: new AllRule([ + new AnyRule([ + new AllRule([ + new AnyRule([ + new AllRule([ + new AnyRule([ + new AllRule([ + new AnyRule([ + new AllRule([ + new AnyRule([ + new AllRule([ + new AnyRule([ + new AnyRule([ + new AllRule([new AnyRule([new FieldRule('username', '*')])]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + maxDepth: 11, + onSwitchEditorMode: jest.fn(), + onChange: jest.fn(), + }; + const wrapper = mountWithIntl(); + expect(findTestSubject(wrapper, 'roleMappingsRulesTooComplex')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx new file mode 100644 index 0000000000000..214c583189fb8 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { EuiEmptyPrompt, EuiCallOut, EuiSpacer, EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FieldRuleEditor } from './field_rule_editor'; +import { RuleGroupEditor } from './rule_group_editor'; +import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; +import { Rule, FieldRule, RuleGroup, AllRule } from '../../../model'; +import { isRuleGroup } from '../../services/is_rule_group'; + +interface Props { + rules: Rule | null; + maxDepth: number; + onChange: (rules: Rule | null) => void; + onSwitchEditorMode: () => void; +} + +export class VisualRuleEditor extends Component { + public render() { + if (this.props.rules) { + const rules = this.renderRule(this.props.rules, this.onRuleChange); + return ( + + {this.getRuleDepthWarning()} + {rules} + + ); + } + + return ( + + + + } + titleSize="s" + body={ +
+ +
+ } + data-test-subj="roleMappingsNoRulesDefined" + actions={ + { + this.props.onChange(new AllRule([new FieldRule('username', '*')])); + }} + > + + + } + /> + ); + } + + private canUseVisualEditor = () => this.props.maxDepth < VISUAL_MAX_RULE_DEPTH; + + private getRuleDepthWarning = () => { + if (this.canUseVisualEditor()) { + return null; + } + return ( + + + } + data-test-subj="roleMappingsRulesTooComplex" + > +

+ +

+ + + + +
+ +
+ ); + }; + + private onRuleChange = (updatedRule: Rule) => { + this.props.onChange(updatedRule); + }; + + private onRuleDelete = () => { + this.props.onChange(null); + }; + + private renderRule = (rule: Rule, onChange: (updatedRule: Rule) => void) => { + return this.getEditorForRuleType(rule, onChange); + }; + + private getEditorForRuleType(rule: Rule, onChange: (updatedRule: Rule) => void) { + if (isRuleGroup(rule)) { + return ( + onChange(value)} + onDelete={this.onRuleDelete} + /> + ); + } + return ( + onChange(value)} + onDelete={this.onRuleDelete} + /> + ); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html new file mode 100644 index 0000000000000..ca8ab9c35c49b --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html @@ -0,0 +1,3 @@ + +
+ diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx new file mode 100644 index 0000000000000..b064a4dc50a22 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import routes from 'ui/routes'; +import { I18nContext } from 'ui/i18n'; +import { npSetup } from 'ui/new_platform'; +import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; +// @ts-ignore +import template from './edit_role_mapping.html'; +import { CREATE_ROLE_MAPPING_PATH } from '../../management_urls'; +import { getEditRoleMappingBreadcrumbs } from '../../breadcrumbs'; +import { EditRoleMappingPage } from './components'; + +routes.when(`${CREATE_ROLE_MAPPING_PATH}/:name?`, { + template, + k7Breadcrumbs: getEditRoleMappingBreadcrumbs, + controller($scope, $route) { + $scope.$$postDigest(() => { + const domNode = document.getElementById('editRoleMappingReactRoot'); + + const { name } = $route.current.params; + + render( + + + , + domNode + ); + + // unmount react on controller destroy + $scope.$on('$destroy', () => { + if (domNode) { + unmountComponentAtNode(domNode); + } + }); + }); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts similarity index 65% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts rename to x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts index 08d8d9a5d4069..60a879c6c29df 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts @@ -4,5 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getTelemetryMonitorPageLogger } from './log_monitor'; -export { getTelemetryOverviewPageLogger } from './log_overview'; +import { Rule, FieldRule } from '../../model'; + +export function isRuleGroup(rule: Rule) { + return !(rule instanceof FieldRule); +} diff --git a/x-pack/legacy/plugins/siem/public/app.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts similarity index 85% rename from x-pack/legacy/plugins/siem/public/app.ts rename to x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts index b068f8a9becda..28010013c9f4f 100644 --- a/x-pack/legacy/plugins/siem/public/app.ts +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './apps/index'; +export const VISUAL_MAX_RULE_DEPTH = 5; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts new file mode 100644 index 0000000000000..9614c4338b631 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + validateRoleMappingName, + validateRoleMappingRoles, + validateRoleMappingRoleTemplates, + validateRoleMappingRules, + validateRoleMappingForSave, +} from './role_mapping_validation'; +import { RoleMapping } from '../../../../../../common/model'; + +describe('validateRoleMappingName', () => { + it('requires a value', () => { + expect(validateRoleMappingName({ name: '' } as RoleMapping)).toMatchInlineSnapshot(` + Object { + "error": "Name is required.", + "isInvalid": true, + } + `); + }); +}); + +describe('validateRoleMappingRoles', () => { + it('requires a value', () => { + expect(validateRoleMappingRoles(({ roles: [] } as unknown) as RoleMapping)) + .toMatchInlineSnapshot(` + Object { + "error": "At least one role is required.", + "isInvalid": true, + } + `); + }); +}); + +describe('validateRoleMappingRoleTemplates', () => { + it('requires a value', () => { + expect(validateRoleMappingRoleTemplates(({ role_templates: [] } as unknown) as RoleMapping)) + .toMatchInlineSnapshot(` + Object { + "error": "At least one role template is required.", + "isInvalid": true, + } + `); + }); +}); + +describe('validateRoleMappingRules', () => { + it('requires at least one rule', () => { + expect(validateRoleMappingRules({ rules: {} } as RoleMapping)).toMatchInlineSnapshot(` + Object { + "error": "At least one rule is required.", + "isInvalid": true, + } + `); + }); + + // more exhaustive testing is done in other unit tests + it('requires rules to be valid', () => { + expect(validateRoleMappingRules(({ rules: { something: [] } } as unknown) as RoleMapping)) + .toMatchInlineSnapshot(` + Object { + "error": "Unknown rule type: something.", + "isInvalid": true, + } + `); + }); +}); + +describe('validateRoleMappingForSave', () => { + it('fails if the role mapping is missing a name', () => { + expect( + validateRoleMappingForSave(({ + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + } as unknown) as RoleMapping) + ).toMatchInlineSnapshot(` + Object { + "error": "Name is required.", + "isInvalid": true, + } + `); + }); + + it('fails if the role mapping is missing rules', () => { + expect( + validateRoleMappingForSave(({ + name: 'foo', + enabled: true, + roles: ['superuser'], + rules: {}, + } as unknown) as RoleMapping) + ).toMatchInlineSnapshot(` + Object { + "error": "At least one rule is required.", + "isInvalid": true, + } + `); + }); + + it('fails if the role mapping is missing both roles and templates', () => { + expect( + validateRoleMappingForSave(({ + name: 'foo', + enabled: true, + roles: [], + role_templates: [], + rules: { field: { username: '*' } }, + } as unknown) as RoleMapping) + ).toMatchInlineSnapshot(` + Object { + "error": "At least one role is required.", + "isInvalid": true, + } + `); + }); + + it('validates a correct role mapping using role templates', () => { + expect( + validateRoleMappingForSave(({ + name: 'foo', + enabled: true, + roles: [], + role_templates: [{ template: { id: 'foo' } }], + rules: { field: { username: '*' } }, + } as unknown) as RoleMapping) + ).toMatchInlineSnapshot(` + Object { + "isInvalid": false, + } + `); + }); + + it('validates a correct role mapping using roles', () => { + expect( + validateRoleMappingForSave(({ + name: 'foo', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + } as unknown) as RoleMapping) + ).toMatchInlineSnapshot(` + Object { + "isInvalid": false, + } + `); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts new file mode 100644 index 0000000000000..5916d6fd9e189 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RoleMapping } from '../../../../../../common/model'; +import { generateRulesFromRaw } from '../../model'; + +interface ValidationResult { + isInvalid: boolean; + error?: string; +} + +export function validateRoleMappingName({ name }: RoleMapping): ValidationResult { + if (!name) { + return invalid( + i18n.translate('xpack.security.role_mappings.validation.invalidName', { + defaultMessage: 'Name is required.', + }) + ); + } + return valid(); +} + +export function validateRoleMappingRoles({ roles }: RoleMapping): ValidationResult { + if (roles && !roles.length) { + return invalid( + i18n.translate('xpack.security.role_mappings.validation.invalidRoles', { + defaultMessage: 'At least one role is required.', + }) + ); + } + return valid(); +} + +export function validateRoleMappingRoleTemplates({ + role_templates: roleTemplates, +}: RoleMapping): ValidationResult { + if (roleTemplates && !roleTemplates.length) { + return invalid( + i18n.translate('xpack.security.role_mappings.validation.invalidRoleTemplates', { + defaultMessage: 'At least one role template is required.', + }) + ); + } + return valid(); +} + +export function validateRoleMappingRules({ rules }: Pick): ValidationResult { + try { + const { rules: parsedRules } = generateRulesFromRaw(rules); + if (!parsedRules) { + return invalid( + i18n.translate('xpack.security.role_mappings.validation.invalidRoleRule', { + defaultMessage: 'At least one rule is required.', + }) + ); + } + } catch (e) { + return invalid(e.message); + } + + return valid(); +} + +export function validateRoleMappingForSave(roleMapping: RoleMapping): ValidationResult { + const { isInvalid: isNameInvalid, error: nameError } = validateRoleMappingName(roleMapping); + const { isInvalid: areRolesInvalid, error: rolesError } = validateRoleMappingRoles(roleMapping); + const { + isInvalid: areRoleTemplatesInvalid, + error: roleTemplatesError, + } = validateRoleMappingRoleTemplates(roleMapping); + + const { isInvalid: areRulesInvalid, error: rulesError } = validateRoleMappingRules(roleMapping); + + const canSave = + !isNameInvalid && (!areRolesInvalid || !areRoleTemplatesInvalid) && !areRulesInvalid; + + if (canSave) { + return valid(); + } + return invalid(nameError || rulesError || rolesError || roleTemplatesError); +} + +function valid() { + return { isInvalid: false }; +} + +function invalid(error?: string) { + return { isInvalid: true, error }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts new file mode 100644 index 0000000000000..8e1f47a4157ae --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.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 { + isStoredRoleTemplate, + isInlineRoleTemplate, + isInvalidRoleTemplate, +} from './role_template_type'; +import { RoleTemplate } from '../../../../../../common/model'; + +describe('#isStoredRoleTemplate', () => { + it('returns true for stored templates, false otherwise', () => { + expect(isStoredRoleTemplate({ template: { id: '' } })).toEqual(true); + expect(isStoredRoleTemplate({ template: { source: '' } })).toEqual(false); + expect(isStoredRoleTemplate({ template: 'asdf' })).toEqual(false); + expect(isStoredRoleTemplate({} as RoleTemplate)).toEqual(false); + }); +}); + +describe('#isInlineRoleTemplate', () => { + it('returns true for inline templates, false otherwise', () => { + expect(isInlineRoleTemplate({ template: { source: '' } })).toEqual(true); + expect(isInlineRoleTemplate({ template: { id: '' } })).toEqual(false); + expect(isInlineRoleTemplate({ template: 'asdf' })).toEqual(false); + expect(isInlineRoleTemplate({} as RoleTemplate)).toEqual(false); + }); +}); + +describe('#isInvalidRoleTemplate', () => { + it('returns true for invalid templates, false otherwise', () => { + expect(isInvalidRoleTemplate({ template: 'asdf' })).toEqual(true); + expect(isInvalidRoleTemplate({} as RoleTemplate)).toEqual(true); + expect(isInvalidRoleTemplate({ template: { source: '' } })).toEqual(false); + expect(isInvalidRoleTemplate({ template: { id: '' } })).toEqual(false); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts new file mode 100644 index 0000000000000..90d8d1a09e587 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + RoleTemplate, + StoredRoleTemplate, + InlineRoleTemplate, + InvalidRoleTemplate, +} from '../../../../../../common/model'; + +export function isStoredRoleTemplate( + roleMappingTemplate: RoleTemplate +): roleMappingTemplate is StoredRoleTemplate { + return ( + roleMappingTemplate.template != null && + roleMappingTemplate.template.hasOwnProperty('id') && + typeof ((roleMappingTemplate as unknown) as StoredRoleTemplate).template.id === 'string' + ); +} + +export function isInlineRoleTemplate( + roleMappingTemplate: RoleTemplate +): roleMappingTemplate is InlineRoleTemplate { + return ( + roleMappingTemplate.template != null && + roleMappingTemplate.template.hasOwnProperty('source') && + typeof ((roleMappingTemplate as unknown) as InlineRoleTemplate).template.source === 'string' + ); +} + +export function isInvalidRoleTemplate( + roleMappingTemplate: RoleTemplate +): roleMappingTemplate is InvalidRoleTemplate { + return !isStoredRoleTemplate(roleMappingTemplate) && !isInlineRoleTemplate(roleMappingTemplate); +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap new file mode 100644 index 0000000000000..1c61383b951ae --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateRulesFromRaw "field" does not support a value of () => null 1`] = `"Invalid value type for field. Expected one of null, string, number, or boolean, but found function ()."`; + +exports[`generateRulesFromRaw "field" does not support a value of [object Object] 1`] = `"Invalid value type for field. Expected one of null, string, number, or boolean, but found object ({})."`; + +exports[`generateRulesFromRaw "field" does not support a value of [object Object],,,() => null 1`] = `"Invalid value type for field. Expected one of null, string, number, or boolean, but found object ([{},null,[],null])."`; + +exports[`generateRulesFromRaw "field" does not support a value of undefined 1`] = `"Invalid value type for field. Expected one of null, string, number, or boolean, but found undefined ()."`; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts new file mode 100644 index 0000000000000..ddf3b4499f73b --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.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 { AllRule, AnyRule, FieldRule, ExceptAllRule, ExceptAnyRule, RuleGroup } from '.'; + +describe('All rule', () => { + it('can be constructed without sub rules', () => { + const rule = new AllRule(); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can be constructed with sub rules', () => { + const rule = new AllRule([new AnyRule()]); + expect(rule.getRules()).toHaveLength(1); + }); + + it('can accept rules of any type', () => { + const subRules = [ + new AllRule(), + new AnyRule(), + new FieldRule('username', '*'), + new ExceptAllRule(), + new ExceptAnyRule(), + ]; + + const rule = new AllRule() as RuleGroup; + expect(rule.canContainRules(subRules)).toEqual(true); + subRules.forEach(sr => rule.addRule(sr)); + expect(rule.getRules()).toEqual([...subRules]); + }); + + it('can replace an existing rule', () => { + const rule = new AllRule([new AnyRule()]); + const newRule = new FieldRule('username', '*'); + rule.replaceRule(0, newRule); + expect(rule.getRules()).toEqual([newRule]); + }); + + it('can remove an existing rule', () => { + const rule = new AllRule([new AnyRule()]); + rule.removeRule(0); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can covert itself into a raw representation', () => { + const rule = new AllRule([new AnyRule()]); + expect(rule.toRaw()).toEqual({ + all: [{ any: [] }], + }); + }); + + it('can clone itself', () => { + const subRules = [new AnyRule()]; + const rule = new AllRule(subRules); + const clone = rule.clone(); + + expect(clone.toRaw()).toEqual(rule.toRaw()); + expect(clone.getRules()).toEqual(rule.getRules()); + expect(clone.getRules()).not.toBe(rule.getRules()); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts new file mode 100644 index 0000000000000..ecea27a7fb87f --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RuleGroup } from './rule_group'; +import { Rule } from './rule'; + +/** + * Represents a group of rules which must all evaluate to true. + */ +export class AllRule extends RuleGroup { + constructor(private rules: Rule[] = []) { + super(); + } + + /** {@see RuleGroup.getRules} */ + public getRules() { + return [...this.rules]; + } + + /** {@see RuleGroup.getDisplayTitle} */ + public getDisplayTitle() { + return i18n.translate('xpack.security.management.editRoleMapping.allRule.displayTitle', { + defaultMessage: 'All are true', + }); + } + + /** {@see RuleGroup.replaceRule} */ + public replaceRule(ruleIndex: number, rule: Rule) { + this.rules.splice(ruleIndex, 1, rule); + } + + /** {@see RuleGroup.removeRule} */ + public removeRule(ruleIndex: number) { + this.rules.splice(ruleIndex, 1); + } + + /** {@see RuleGroup.addRule} */ + public addRule(rule: Rule) { + this.rules.push(rule); + } + + /** {@see RuleGroup.canContainRules} */ + public canContainRules() { + return true; + } + + /** {@see RuleGroup.clone} */ + public clone() { + return new AllRule(this.rules.map(r => r.clone())); + } + + /** {@see RuleGroup.toRaw} */ + public toRaw() { + return { + all: [...this.rules.map(rule => rule.toRaw())], + }; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts new file mode 100644 index 0000000000000..767aa075755af --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AllRule, AnyRule, FieldRule, ExceptAllRule, ExceptAnyRule, RuleGroup } from '.'; + +describe('Any rule', () => { + it('can be constructed without sub rules', () => { + const rule = new AnyRule(); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can be constructed with sub rules', () => { + const rule = new AnyRule([new AllRule()]); + expect(rule.getRules()).toHaveLength(1); + }); + + it('can accept non-except rules', () => { + const subRules = [new AllRule(), new AnyRule(), new FieldRule('username', '*')]; + + const rule = new AnyRule() as RuleGroup; + expect(rule.canContainRules(subRules)).toEqual(true); + subRules.forEach(sr => rule.addRule(sr)); + expect(rule.getRules()).toEqual([...subRules]); + }); + + it('cannot accept except rules', () => { + const subRules = [new ExceptAllRule(), new ExceptAnyRule()]; + + const rule = new AnyRule() as RuleGroup; + expect(rule.canContainRules(subRules)).toEqual(false); + }); + + it('can replace an existing rule', () => { + const rule = new AnyRule([new AllRule()]); + const newRule = new FieldRule('username', '*'); + rule.replaceRule(0, newRule); + expect(rule.getRules()).toEqual([newRule]); + }); + + it('can remove an existing rule', () => { + const rule = new AnyRule([new AllRule()]); + rule.removeRule(0); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can covert itself into a raw representation', () => { + const rule = new AnyRule([new AllRule()]); + expect(rule.toRaw()).toEqual({ + any: [{ all: [] }], + }); + }); + + it('can clone itself', () => { + const subRules = [new AllRule()]; + const rule = new AnyRule(subRules); + const clone = rule.clone(); + + expect(clone.toRaw()).toEqual(rule.toRaw()); + expect(clone.getRules()).toEqual(rule.getRules()); + expect(clone.getRules()).not.toBe(rule.getRules()); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts new file mode 100644 index 0000000000000..6a4f2eaf1b362 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RuleGroup } from './rule_group'; +import { Rule } from './rule'; +import { ExceptAllRule } from './except_all_rule'; +import { ExceptAnyRule } from './except_any_rule'; + +/** + * Represents a group of rules in which at least one must evaluate to true. + */ +export class AnyRule extends RuleGroup { + constructor(private rules: Rule[] = []) { + super(); + } + + /** {@see RuleGroup.getRules} */ + public getRules() { + return [...this.rules]; + } + + /** {@see RuleGroup.getDisplayTitle} */ + public getDisplayTitle() { + return i18n.translate('xpack.security.management.editRoleMapping.anyRule.displayTitle', { + defaultMessage: 'Any are true', + }); + } + + /** {@see RuleGroup.replaceRule} */ + public replaceRule(ruleIndex: number, rule: Rule) { + this.rules.splice(ruleIndex, 1, rule); + } + + /** {@see RuleGroup.removeRule} */ + public removeRule(ruleIndex: number) { + this.rules.splice(ruleIndex, 1); + } + + /** {@see RuleGroup.addRule} */ + public addRule(rule: Rule) { + this.rules.push(rule); + } + + /** {@see RuleGroup.canContainRules} */ + public canContainRules(rules: Rule[]) { + const forbiddenRules = [ExceptAllRule, ExceptAnyRule]; + return rules.every( + candidate => !forbiddenRules.some(forbidden => candidate instanceof forbidden) + ); + } + + /** {@see RuleGroup.clone} */ + public clone() { + return new AnyRule(this.rules.map(r => r.clone())); + } + + /** {@see RuleGroup.toRaw} */ + public toRaw() { + return { + any: [...this.rules.map(rule => rule.toRaw())], + }; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts new file mode 100644 index 0000000000000..7a00e5b19638f --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.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 { AllRule, AnyRule, FieldRule, ExceptAllRule, ExceptAnyRule, RuleGroup } from '.'; + +describe('Except All rule', () => { + it('can be constructed without sub rules', () => { + const rule = new ExceptAllRule(); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can be constructed with sub rules', () => { + const rule = new ExceptAllRule([new AnyRule()]); + expect(rule.getRules()).toHaveLength(1); + }); + + it('can accept rules of any type', () => { + const subRules = [ + new AllRule(), + new AnyRule(), + new FieldRule('username', '*'), + new ExceptAllRule(), + new ExceptAnyRule(), + ]; + + const rule = new ExceptAllRule() as RuleGroup; + expect(rule.canContainRules(subRules)).toEqual(true); + subRules.forEach(sr => rule.addRule(sr)); + expect(rule.getRules()).toEqual([...subRules]); + }); + + it('can replace an existing rule', () => { + const rule = new ExceptAllRule([new AnyRule()]); + const newRule = new FieldRule('username', '*'); + rule.replaceRule(0, newRule); + expect(rule.getRules()).toEqual([newRule]); + }); + + it('can remove an existing rule', () => { + const rule = new ExceptAllRule([new AnyRule()]); + rule.removeRule(0); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can covert itself into a raw representation', () => { + const rule = new ExceptAllRule([new AnyRule()]); + expect(rule.toRaw()).toEqual({ + except: { all: [{ any: [] }] }, + }); + }); + + it('can clone itself', () => { + const subRules = [new AllRule()]; + const rule = new ExceptAllRule(subRules); + const clone = rule.clone(); + + expect(clone.toRaw()).toEqual(rule.toRaw()); + expect(clone.getRules()).toEqual(rule.getRules()); + expect(clone.getRules()).not.toBe(rule.getRules()); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts new file mode 100644 index 0000000000000..a67c2622a2533 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RuleGroup } from './rule_group'; +import { Rule } from './rule'; + +/** + * Represents a group of rules in which at least one must evaluate to false. + */ +export class ExceptAllRule extends RuleGroup { + constructor(private rules: Rule[] = []) { + super(); + } + + /** {@see RuleGroup.getRules} */ + public getRules() { + return [...this.rules]; + } + + /** {@see RuleGroup.getDisplayTitle} */ + public getDisplayTitle() { + return i18n.translate('xpack.security.management.editRoleMapping.exceptAllRule.displayTitle', { + defaultMessage: 'Any are false', + }); + } + + /** {@see RuleGroup.replaceRule} */ + public replaceRule(ruleIndex: number, rule: Rule) { + this.rules.splice(ruleIndex, 1, rule); + } + + /** {@see RuleGroup.removeRule} */ + public removeRule(ruleIndex: number) { + this.rules.splice(ruleIndex, 1); + } + + /** {@see RuleGroup.addRule} */ + public addRule(rule: Rule) { + this.rules.push(rule); + } + + /** {@see RuleGroup.canContainRules} */ + public canContainRules() { + return true; + } + + /** {@see RuleGroup.clone} */ + public clone() { + return new ExceptAllRule(this.rules.map(r => r.clone())); + } + + /** {@see RuleGroup.toRaw} */ + public toRaw() { + const rawRule = { + all: [...this.rules.map(rule => rule.toRaw())], + }; + + return { + except: rawRule, + }; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts new file mode 100644 index 0000000000000..e4e182ce88d8d --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AllRule, AnyRule, FieldRule, ExceptAllRule, ExceptAnyRule, RuleGroup } from '.'; + +describe('Except Any rule', () => { + it('can be constructed without sub rules', () => { + const rule = new ExceptAnyRule(); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can be constructed with sub rules', () => { + const rule = new ExceptAnyRule([new AllRule()]); + expect(rule.getRules()).toHaveLength(1); + }); + + it('can accept non-except rules', () => { + const subRules = [new AllRule(), new AnyRule(), new FieldRule('username', '*')]; + + const rule = new ExceptAnyRule() as RuleGroup; + expect(rule.canContainRules(subRules)).toEqual(true); + subRules.forEach(sr => rule.addRule(sr)); + expect(rule.getRules()).toEqual([...subRules]); + }); + + it('cannot accept except rules', () => { + const subRules = [new ExceptAllRule(), new ExceptAnyRule()]; + + const rule = new ExceptAnyRule() as RuleGroup; + expect(rule.canContainRules(subRules)).toEqual(false); + }); + + it('can replace an existing rule', () => { + const rule = new ExceptAnyRule([new AllRule()]); + const newRule = new FieldRule('username', '*'); + rule.replaceRule(0, newRule); + expect(rule.getRules()).toEqual([newRule]); + }); + + it('can remove an existing rule', () => { + const rule = new ExceptAnyRule([new AllRule()]); + rule.removeRule(0); + expect(rule.getRules()).toHaveLength(0); + }); + + it('can covert itself into a raw representation', () => { + const rule = new ExceptAnyRule([new AllRule()]); + expect(rule.toRaw()).toEqual({ + except: { any: [{ all: [] }] }, + }); + }); + + it('can clone itself', () => { + const subRules = [new AllRule()]; + const rule = new ExceptAnyRule(subRules); + const clone = rule.clone(); + + expect(clone.toRaw()).toEqual(rule.toRaw()); + expect(clone.getRules()).toEqual(rule.getRules()); + expect(clone.getRules()).not.toBe(rule.getRules()); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts new file mode 100644 index 0000000000000..12ec3fe85b80d --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RuleGroup } from './rule_group'; +import { Rule } from './rule'; +import { ExceptAllRule } from './except_all_rule'; + +/** + * Represents a group of rules in which none can evaluate to true (all must evaluate to false). + */ +export class ExceptAnyRule extends RuleGroup { + constructor(private rules: Rule[] = []) { + super(); + } + + /** {@see RuleGroup.getRules} */ + public getRules() { + return [...this.rules]; + } + + /** {@see RuleGroup.getDisplayTitle} */ + public getDisplayTitle() { + return i18n.translate('xpack.security.management.editRoleMapping.exceptAnyRule.displayTitle', { + defaultMessage: 'All are false', + }); + } + + /** {@see RuleGroup.replaceRule} */ + public replaceRule(ruleIndex: number, rule: Rule) { + this.rules.splice(ruleIndex, 1, rule); + } + + /** {@see RuleGroup.removeRule} */ + public removeRule(ruleIndex: number) { + this.rules.splice(ruleIndex, 1); + } + + /** {@see RuleGroup.addRule} */ + public addRule(rule: Rule) { + this.rules.push(rule); + } + + /** {@see RuleGroup.canContainRules} */ + public canContainRules(rules: Rule[]) { + const forbiddenRules = [ExceptAllRule, ExceptAnyRule]; + return rules.every( + candidate => !forbiddenRules.some(forbidden => candidate instanceof forbidden) + ); + } + + /** {@see RuleGroup.clone} */ + public clone() { + return new ExceptAnyRule(this.rules.map(r => r.clone())); + } + + /** {@see RuleGroup.toRaw} */ + public toRaw() { + const rawRule = { + any: [...this.rules.map(rule => rule.toRaw())], + }; + + return { + except: rawRule, + }; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts new file mode 100644 index 0000000000000..3447e81707002 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.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 { FieldRule } from '.'; + +describe('FieldRule', () => { + ['*', 1, null, true, false].forEach(value => { + it(`can convert itself to raw form with a single value of ${value}`, () => { + const rule = new FieldRule('username', value); + expect(rule.toRaw()).toEqual({ + field: { + username: value, + }, + }); + }); + }); + + it('can convert itself to raw form with an array of values', () => { + const values = ['*', 1, null, true, false]; + const rule = new FieldRule('username', values); + const raw = rule.toRaw(); + expect(raw).toEqual({ + field: { + username: ['*', 1, null, true, false], + }, + }); + + // shoud not be the same array instance + expect(raw.field.username).not.toBe(values); + }); + + it('can clone itself', () => { + const values = ['*', 1, null]; + const rule = new FieldRule('username', values); + + const clone = rule.clone(); + expect(clone.field).toEqual(rule.field); + expect(clone.value).toEqual(rule.value); + expect(clone.value).not.toBe(rule.value); + expect(clone.toRaw()).toEqual(rule.toRaw()); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts new file mode 100644 index 0000000000000..3e6a0e1e7ecb3 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { Rule } from './rule'; + +/** The allowed types for field rule values */ +export type FieldRuleValue = + | string + | number + | null + | boolean + | Array; + +/** + * Represents a single field rule. + * Ex: "username = 'foo'" + */ +export class FieldRule extends Rule { + constructor(public readonly field: string, public readonly value: FieldRuleValue) { + super(); + } + + /** {@see Rule.getDisplayTitle} */ + public getDisplayTitle() { + return i18n.translate('xpack.security.management.editRoleMapping.fieldRule.displayTitle', { + defaultMessage: 'The following is true', + }); + } + + /** {@see Rule.clone} */ + public clone() { + return new FieldRule(this.field, Array.isArray(this.value) ? [...this.value] : this.value); + } + + /** {@see Rule.toRaw} */ + public toRaw() { + return { + field: { + [this.field]: Array.isArray(this.value) ? [...this.value] : this.value, + }, + }; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts new file mode 100644 index 0000000000000..cbc970f02b03e --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AllRule } from './all_rule'; +export { AnyRule } from './any_rule'; +export { Rule } from './rule'; +export { RuleGroup } from './rule_group'; +export { ExceptAllRule } from './except_all_rule'; +export { ExceptAnyRule } from './except_any_rule'; +export { FieldRule, FieldRuleValue } from './field_rule'; +export { generateRulesFromRaw } from './rule_builder'; +export { RuleBuilderError } from './rule_builder_error'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts new file mode 100644 index 0000000000000..5cab2f1736e94 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Represents a Role Mapping rule. + */ +export abstract class Rule { + /** + * Converts this rule into a raw object for use in the persisted Role Mapping. + */ + abstract toRaw(): Record; + + /** + * The display title for this rule. + */ + abstract getDisplayTitle(): string; + + /** + * Returns a new instance of this rule. + */ + abstract clone(): Rule; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts new file mode 100644 index 0000000000000..ebd48f6d15d99 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts @@ -0,0 +1,343 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { generateRulesFromRaw, FieldRule } from '.'; +import { RoleMapping } from '../../../../../common/model'; +import { RuleBuilderError } from './rule_builder_error'; + +describe('generateRulesFromRaw', () => { + it('returns null for an empty rule set', () => { + expect(generateRulesFromRaw({})).toEqual({ + rules: null, + maxDepth: 0, + }); + }); + + it('returns a correctly parsed rule set', () => { + const rawRules: RoleMapping['rules'] = { + all: [ + { + except: { + all: [ + { + field: { username: '*' }, + }, + ], + }, + }, + { + any: [ + { + field: { dn: '*' }, + }, + ], + }, + ], + }; + + const { rules, maxDepth } = generateRulesFromRaw(rawRules); + + expect(rules).toMatchInlineSnapshot(` + AllRule { + "rules": Array [ + ExceptAllRule { + "rules": Array [ + FieldRule { + "field": "username", + "value": "*", + }, + ], + }, + AnyRule { + "rules": Array [ + FieldRule { + "field": "dn", + "value": "*", + }, + ], + }, + ], + } + `); + expect(maxDepth).toEqual(3); + }); + + it('does not support multiple rules at the root level', () => { + expect(() => { + generateRulesFromRaw({ + all: [ + { + field: { username: '*' }, + }, + ], + any: [ + { + field: { username: '*' }, + }, + ], + }); + }).toThrowError('Expected a single rule definition, but found 2.'); + }); + + it('provides a rule trace describing the location of the error', () => { + try { + generateRulesFromRaw({ + all: [ + { + field: { username: '*' }, + }, + { + any: [ + { + field: { username: '*' }, + }, + { + except: { field: { username: '*' } }, + }, + ], + }, + ], + }); + throw new Error(`Expected generateRulesFromRaw to throw error.`); + } catch (e) { + if (e instanceof RuleBuilderError) { + expect(e.message).toEqual(`"except" rule can only exist within an "all" rule.`); + expect(e.ruleTrace).toEqual(['all', '[1]', 'any', '[1]', 'except']); + } else { + throw e; + } + } + }); + + it('calculates the max depth of the rule tree', () => { + const rules = { + all: [ + // depth = 1 + { + // depth = 2 + all: [ + // depth = 3 + { + any: [ + // depth == 4 + { field: { username: 'foo' } }, + ], + }, + { except: { field: { username: 'foo' } } }, + ], + }, + { + // depth = 2 + any: [ + { + // depth = 3 + all: [ + { + // depth = 4 + any: [ + { + // depth = 5 + all: [ + { + // depth = 6 + all: [ + // depth = 7 + { + except: { + field: { username: 'foo' }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + expect(generateRulesFromRaw(rules).maxDepth).toEqual(7); + }); + + describe('"any"', () => { + it('expects an array value', () => { + expect(() => { + generateRulesFromRaw({ + any: { + field: { username: '*' }, + } as any, + }); + }).toThrowError('Expected an array of rules, but found object.'); + }); + + it('expects each entry to be an object with a single property', () => { + expect(() => { + generateRulesFromRaw({ + any: [ + { + any: [{ field: { foo: 'bar' } }], + all: [{ field: { foo: 'bar' } }], + } as any, + ], + }); + }).toThrowError('Expected a single rule definition, but found 2.'); + }); + }); + + describe('"all"', () => { + it('expects an array value', () => { + expect(() => { + generateRulesFromRaw({ + all: { + field: { username: '*' }, + } as any, + }); + }).toThrowError('Expected an array of rules, but found object.'); + }); + + it('expects each entry to be an object with a single property', () => { + expect(() => { + generateRulesFromRaw({ + all: [ + { + field: { username: '*' }, + any: [{ field: { foo: 'bar' } }], + } as any, + ], + }); + }).toThrowError('Expected a single rule definition, but found 2.'); + }); + }); + + describe('"field"', () => { + it(`expects an object value`, () => { + expect(() => { + generateRulesFromRaw({ + field: [ + { + username: '*', + }, + ], + }); + }).toThrowError('Expected an object, but found array.'); + }); + + it(`expects an single property in its object value`, () => { + expect(() => { + generateRulesFromRaw({ + field: { + username: '*', + dn: '*', + }, + }); + }).toThrowError('Expected a single field, but found 2.'); + }); + + it('accepts an array of possible values', () => { + const { rules } = generateRulesFromRaw({ + field: { + username: [0, '*', null, 'foo', true, false], + }, + }); + + expect(rules).toBeInstanceOf(FieldRule); + expect((rules as FieldRule).field).toEqual('username'); + expect((rules as FieldRule).value).toEqual([0, '*', null, 'foo', true, false]); + }); + + [{}, () => null, undefined, [{}, undefined, [], () => null]].forEach(invalidValue => { + it(`does not support a value of ${invalidValue}`, () => { + expect(() => { + generateRulesFromRaw({ + field: { + username: invalidValue, + }, + }); + }).toThrowErrorMatchingSnapshot(); + }); + }); + }); + + describe('"except"', () => { + it(`expects an object value`, () => { + expect(() => { + generateRulesFromRaw({ + all: [ + { + except: [ + { + field: { username: '*' }, + }, + ], + }, + ], + } as any); + }).toThrowError('Expected an object, but found array.'); + }); + + it(`can only be nested inside an "all" clause`, () => { + expect(() => { + generateRulesFromRaw({ + any: [ + { + except: { + field: { + username: '*', + }, + }, + }, + ], + }); + }).toThrowError(`"except" rule can only exist within an "all" rule.`); + + expect(() => { + generateRulesFromRaw({ + except: { + field: { + username: '*', + }, + }, + }); + }).toThrowError(`"except" rule can only exist within an "all" rule.`); + }); + + it('converts an "except field" rule into an equivilent "except all" rule', () => { + expect( + generateRulesFromRaw({ + all: [ + { + except: { + field: { + username: '*', + }, + }, + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "maxDepth": 2, + "rules": AllRule { + "rules": Array [ + ExceptAllRule { + "rules": Array [ + FieldRule { + "field": "username", + "value": "*", + }, + ], + }, + ], + }, + } + `); + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts new file mode 100644 index 0000000000000..fe344b2ae38dd --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RoleMapping } from '../../../../../common/model'; +import { FieldRule, FieldRuleValue } from './field_rule'; +import { AllRule } from './all_rule'; +import { AnyRule } from './any_rule'; +import { Rule } from './rule'; +import { ExceptAllRule } from './except_all_rule'; +import { ExceptAnyRule } from './except_any_rule'; +import { RuleBuilderError } from '.'; + +interface RuleBuilderResult { + /** The maximum rule depth within the parsed rule set. */ + maxDepth: number; + + /** The parsed rule set. */ + rules: Rule | null; +} + +/** + * Given a set of raw rules, this constructs a class based tree for consumption by the Role Management UI. + * This also performs validation on the raw rule set, as it is possible to enter raw JSON in the JSONRuleEditor, + * so we have no guarantees that the rule set is valid ahead of time. + * + * @param rawRules the raw rules to translate. + */ +export function generateRulesFromRaw(rawRules: RoleMapping['rules'] = {}): RuleBuilderResult { + return parseRawRules(rawRules, null, [], 0); +} + +function parseRawRules( + rawRules: RoleMapping['rules'], + parentRuleType: string | null, + ruleTrace: string[], + depth: number +): RuleBuilderResult { + const entries = Object.entries(rawRules); + if (!entries.length) { + return { + rules: null, + maxDepth: 0, + }; + } + if (entries.length > 1) { + throw new RuleBuilderError( + i18n.translate('xpack.security.management.editRoleMapping.ruleBuilder.expectSingleRule', { + defaultMessage: `Expected a single rule definition, but found {numberOfRules}.`, + values: { numberOfRules: entries.length }, + }), + ruleTrace + ); + } + + const rule = entries[0]; + const [ruleType, ruleDefinition] = rule; + return createRuleForType(ruleType, ruleDefinition, parentRuleType, ruleTrace, depth + 1); +} + +function createRuleForType( + ruleType: string, + ruleDefinition: any, + parentRuleType: string | null, + ruleTrace: string[] = [], + depth: number +): RuleBuilderResult { + const isRuleNegated = parentRuleType === 'except'; + + const currentRuleTrace = [...ruleTrace, ruleType]; + + switch (ruleType) { + case 'field': { + assertIsObject(ruleDefinition, currentRuleTrace); + + const entries = Object.entries(ruleDefinition); + if (entries.length !== 1) { + throw new RuleBuilderError( + i18n.translate( + 'xpack.security.management.editRoleMapping.ruleBuilder.expectedSingleFieldRule', + { + defaultMessage: `Expected a single field, but found {count}.`, + values: { count: entries.length }, + } + ), + currentRuleTrace + ); + } + + const [field, value] = entries[0] as [string, FieldRuleValue]; + const values = Array.isArray(value) ? value : [value]; + values.forEach(fieldValue => { + const valueType = typeof fieldValue; + if (fieldValue !== null && !['string', 'number', 'boolean'].includes(valueType)) { + throw new RuleBuilderError( + i18n.translate( + 'xpack.security.management.editRoleMapping.ruleBuilder.invalidFieldValueType', + { + defaultMessage: `Invalid value type for field. Expected one of null, string, number, or boolean, but found {valueType} ({value}).`, + values: { valueType, value: JSON.stringify(value) }, + } + ), + currentRuleTrace + ); + } + }); + + const fieldRule = new FieldRule(field, value); + return { + rules: isRuleNegated ? new ExceptAllRule([fieldRule]) : fieldRule, + maxDepth: depth, + }; + } + case 'any': // intentional fall-through to 'all', as validation logic is identical + case 'all': { + if (ruleDefinition != null && !Array.isArray(ruleDefinition)) { + throw new RuleBuilderError( + i18n.translate( + 'xpack.security.management.editRoleMapping.ruleBuilder.expectedArrayForGroupRule', + { + defaultMessage: `Expected an array of rules, but found {type}.`, + values: { type: typeof ruleDefinition }, + } + ), + currentRuleTrace + ); + } + + const subRulesResults = ((ruleDefinition as any[]) || []).map((definition: any, index) => + parseRawRules(definition, ruleType, [...currentRuleTrace, `[${index}]`], depth) + ) as RuleBuilderResult[]; + + const { subRules, maxDepth } = subRulesResults.reduce( + (acc, result) => { + return { + subRules: [...acc.subRules, result.rules!], + maxDepth: Math.max(acc.maxDepth, result.maxDepth), + }; + }, + { subRules: [] as Rule[], maxDepth: 0 } + ); + + if (ruleType === 'all') { + return { + rules: isRuleNegated ? new ExceptAllRule(subRules) : new AllRule(subRules), + maxDepth, + }; + } else { + return { + rules: isRuleNegated ? new ExceptAnyRule(subRules) : new AnyRule(subRules), + maxDepth, + }; + } + } + case 'except': { + assertIsObject(ruleDefinition, currentRuleTrace); + + if (parentRuleType !== 'all') { + throw new RuleBuilderError( + i18n.translate( + 'xpack.security.management.editRoleMapping.ruleBuilder.exceptOnlyInAllRule', + { + defaultMessage: `"except" rule can only exist within an "all" rule.`, + } + ), + currentRuleTrace + ); + } + // subtracting 1 from depth because we don't currently count the "except" level itself as part of the depth calculation + // for the purpose of determining if the rule set is "too complex" for the visual rule editor. + // The "except" rule MUST be nested within an "all" rule type (see validation above), so the depth itself will always be a non-negative number. + return parseRawRules(ruleDefinition || {}, ruleType, currentRuleTrace, depth - 1); + } + default: + throw new RuleBuilderError( + i18n.translate('xpack.security.management.editRoleMapping.ruleBuilder.unknownRuleType', { + defaultMessage: `Unknown rule type: {ruleType}.`, + values: { ruleType }, + }), + currentRuleTrace + ); + } +} + +function assertIsObject(ruleDefinition: any, ruleTrace: string[]) { + let fieldType: string = typeof ruleDefinition; + if (Array.isArray(ruleDefinition)) { + fieldType = 'array'; + } + + if (ruleDefinition && fieldType !== 'object') { + throw new RuleBuilderError( + i18n.translate('xpack.security.management.editRoleMapping.ruleBuilder.expectedObjectError', { + defaultMessage: `Expected an object, but found {type}.`, + values: { type: fieldType }, + }), + ruleTrace + ); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts new file mode 100644 index 0000000000000..87d73cde00dd6 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Describes an error during rule building. + * In addition to a user-"friendly" message, this also includes a rule trace, + * which is the "JSON path" where the error occurred. + */ +export class RuleBuilderError extends Error { + constructor(message: string, public readonly ruleTrace: string[]) { + super(message); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, RuleBuilderError.prototype); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts new file mode 100644 index 0000000000000..3e1e7fad9b36f --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Rule } from './rule'; + +/** + * Represents a catagory of Role Mapping rules which are capable of containing other rules. + */ +export abstract class RuleGroup extends Rule { + /** + * Returns all immediate sub-rules within this group (non-recursive). + */ + abstract getRules(): Rule[]; + + /** + * Replaces the rule at the indicated location. + * @param ruleIndex the location of the rule to replace. + * @param rule the new rule. + */ + abstract replaceRule(ruleIndex: number, rule: Rule): void; + + /** + * Removes the rule at the indicated location. + * @param ruleIndex the location of the rule to remove. + */ + abstract removeRule(ruleIndex: number): void; + + /** + * Adds a rule to this group. + * @param rule the rule to add. + */ + abstract addRule(rule: Rule): void; + + /** + * Determines if the provided rules are allowed to be contained within this group. + * @param rules the rules to test. + */ + abstract canContainRules(rules: Rule[]): boolean; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx new file mode 100644 index 0000000000000..2342eeb97d03e --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getCreateRoleMappingHref } from '../../../../management_urls'; + +export const CreateRoleMappingButton = () => { + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts new file mode 100644 index 0000000000000..417bf50d754e6 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CreateRoleMappingButton } from './create_role_mapping_button'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx new file mode 100644 index 0000000000000..1919d3fbf4785 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CreateRoleMappingButton } from '../create_role_mapping_button'; + +export const EmptyPrompt: React.FunctionComponent<{}> = () => ( + + + + } + body={ + +

+ +

+
+ } + actions={} + data-test-subj="roleMappingsEmptyPrompt" + /> +); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts new file mode 100644 index 0000000000000..982e34a0ceed5 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EmptyPrompt } from './empty_prompt'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts new file mode 100644 index 0000000000000..4bd5df71da446 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RoleMappingsGridPage } from './role_mappings_grid_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx new file mode 100644 index 0000000000000..259cdc71e25a2 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { RoleMappingsGridPage } from '.'; +import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../../components'; +import { EmptyPrompt } from './empty_prompt'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiLink } from '@elastic/eui'; +import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { act } from '@testing-library/react'; + +describe('RoleMappingsGridPage', () => { + it('renders an empty prompt when no role mappings exist', async () => { + const roleMappingsAPI = ({ + getRoleMappings: jest.fn().mockResolvedValue([]), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(EmptyPrompt)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(EmptyPrompt)).toHaveLength(1); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = ({ + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = ({ + getRoleMappings: jest.fn().mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: [], + rules: { field: { username: '*' } }, + }, + ]), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders links to mapped roles', async () => { + const roleMappingsAPI = ({ + getRoleMappings: jest.fn().mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); + expect(links).toHaveLength(1); + expect(links.at(0).props()).toMatchObject({ + href: '#/management/security/roles/edit/superuser', + }); + }); + + it('describes the number of mapped role templates', async () => { + const roleMappingsAPI = ({ + getRoleMappings: jest.fn().mockResolvedValue([ + { + name: 'some realm', + enabled: true, + role_templates: [{}, {}], + rules: { field: { username: '*' } }, + }, + ]), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + const templates = findTestSubject(wrapper, 'roleMappingRoles'); + expect(templates).toHaveLength(1); + expect(templates.text()).toEqual(`2 role templates defined`); + }); + + it('allows role mappings to be deleted, refreshing the grid after', async () => { + const roleMappingsAPI = ({ + getRoleMappings: jest.fn().mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]), + checkRoleMappingFeatures: jest.fn().mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }), + deleteRoleMappings: jest.fn().mockReturnValue( + Promise.resolve([ + { + name: 'some-realm', + success: true, + }, + ]) + ), + } as unknown) as RoleMappingsAPI; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); + expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); + + findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); + expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); + // Expect an additional API call to refresh the grid + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx new file mode 100644 index 0000000000000..7b23f2288d1ba --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx @@ -0,0 +1,474 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { + EuiBadge, + EuiButton, + EuiButtonIcon, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, + EuiLink, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RoleMapping } from '../../../../../../common/model'; +import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { EmptyPrompt } from './empty_prompt'; +import { + NoCompatibleRealms, + DeleteProvider, + PermissionDenied, + SectionLoading, +} from '../../components'; +import { documentationLinks } from '../../services/documentation_links'; +import { + getCreateRoleMappingHref, + getEditRoleMappingHref, + getEditRoleHref, +} from '../../../management_urls'; + +interface Props { + roleMappingsAPI: RoleMappingsAPI; +} + +interface State { + loadState: 'loadingApp' | 'loadingTable' | 'permissionDenied' | 'finished'; + roleMappings: null | RoleMapping[]; + selectedItems: RoleMapping[]; + hasCompatibleRealms: boolean; + error: any; +} + +export class RoleMappingsGridPage extends Component { + constructor(props: any) { + super(props); + this.state = { + loadState: 'loadingApp', + roleMappings: null, + hasCompatibleRealms: true, + selectedItems: [], + error: undefined, + }; + } + + public componentDidMount() { + this.checkPrivileges(); + } + + public render() { + const { loadState, error, roleMappings } = this.state; + + if (loadState === 'permissionDenied') { + return ; + } + + if (loadState === 'loadingApp') { + return ( + + + + + + ); + } + + if (error) { + const { + body: { error: errorTitle, message, statusCode }, + } = error; + + return ( + + + } + color="danger" + iconType="alert" + > + {statusCode}: {errorTitle} - {message} + + + ); + } + + if (loadState === 'finished' && roleMappings && roleMappings.length === 0) { + return ( + + + + ); + } + + return ( + + + + +

+ +

+
+ +

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

+
+
+ + + + + +
+ + + {!this.state.hasCompatibleRealms && ( + <> + + + + )} + {this.renderTable()} + + +
+ ); + } + + private renderTable = () => { + const { roleMappings, selectedItems, loadState } = this.state; + + const message = + loadState === 'loadingTable' ? ( + + ) : ( + undefined + ); + + const sorting = { + sort: { + field: 'name', + direction: 'asc' as any, + }, + }; + + const pagination = { + initialPageSize: 20, + pageSizeOptions: [10, 20, 50], + }; + + const selection = { + onSelectionChange: (newSelectedItems: RoleMapping[]) => { + this.setState({ + selectedItems: newSelectedItems, + }); + }, + }; + + const search = { + toolsLeft: selectedItems.length ? ( + + {deleteRoleMappingsPrompt => { + return ( + deleteRoleMappingsPrompt(selectedItems, this.onRoleMappingsDeleted)} + color="danger" + data-test-subj="bulkDeleteActionButton" + > + + + ); + }} + + ) : ( + undefined + ), + toolsRight: ( + this.reloadRoleMappings()} + data-test-subj="reloadButton" + > + + + ), + box: { + incremental: true, + }, + filters: undefined, + }; + + return ( + { + return { + 'data-test-subj': 'roleMappingRow', + }; + }} + /> + ); + }; + + private getColumnConfig = () => { + const config = [ + { + field: 'name', + name: i18n.translate('xpack.security.management.roleMappings.nameColumnName', { + defaultMessage: 'Name', + }), + sortable: true, + render: (roleMappingName: string) => { + return ( + + {roleMappingName} + + ); + }, + }, + { + field: 'roles', + name: i18n.translate('xpack.security.management.roleMappings.rolesColumnName', { + defaultMessage: 'Roles', + }), + sortable: true, + render: (entry: any, record: RoleMapping) => { + const { roles = [], role_templates: roleTemplates = [] } = record; + if (roleTemplates.length > 0) { + return ( + + {i18n.translate('xpack.security.management.roleMappings.roleTemplates', { + defaultMessage: + '{templateCount, plural, one{# role template} other {# role templates}} defined', + values: { + templateCount: roleTemplates.length, + }, + })} + + ); + } + const roleLinks = roles.map((rolename, index) => { + return ( + + {rolename} + {index === roles.length - 1 ? null : ', '} + + ); + }); + return
{roleLinks}
; + }, + }, + { + field: 'enabled', + name: i18n.translate('xpack.security.management.roleMappings.enabledColumnName', { + defaultMessage: 'Enabled', + }), + sortable: true, + render: (enabled: boolean) => { + if (enabled) { + return ( + + + + ); + } + + return ( + + + + ); + }, + }, + { + name: i18n.translate('xpack.security.management.roleMappings.actionsColumnName', { + defaultMessage: 'Actions', + }), + actions: [ + { + render: (record: RoleMapping) => { + return ( + + + + ); + }, + }, + { + render: (record: RoleMapping) => { + return ( + + + + {deleteRoleMappingPrompt => { + return ( + + + deleteRoleMappingPrompt([record], this.onRoleMappingsDeleted) + } + /> + + ); + }} + + + + ); + }, + }, + ], + }, + ]; + return config; + }; + + private onRoleMappingsDeleted = (roleMappings: string[]): void => { + if (roleMappings.length) { + this.reloadRoleMappings(); + } + }; + + private async checkPrivileges() { + try { + const { + canManageRoleMappings, + hasCompatibleRealms, + } = await this.props.roleMappingsAPI.checkRoleMappingFeatures(); + + this.setState({ + loadState: canManageRoleMappings ? this.state.loadState : 'permissionDenied', + hasCompatibleRealms, + }); + + if (canManageRoleMappings) { + this.loadRoleMappings(); + } + } catch (e) { + this.setState({ error: e, loadState: 'finished' }); + } + } + + private reloadRoleMappings = () => { + this.setState({ roleMappings: [], loadState: 'loadingTable' }); + this.loadRoleMappings(); + }; + + private loadRoleMappings = async () => { + try { + const roleMappings = await this.props.roleMappingsAPI.getRoleMappings(); + this.setState({ roleMappings }); + } catch (e) { + this.setState({ error: e }); + } + + this.setState({ loadState: 'finished' }); + }; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx new file mode 100644 index 0000000000000..9e925d0fa6dc0 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx @@ -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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import routes from 'ui/routes'; +import { I18nContext } from 'ui/i18n'; +import { npSetup } from 'ui/new_platform'; +import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; +// @ts-ignore +import template from './role_mappings.html'; +import { ROLE_MAPPINGS_PATH } from '../../management_urls'; +import { getRoleMappingBreadcrumbs } from '../../breadcrumbs'; +import { RoleMappingsGridPage } from './components'; + +routes.when(ROLE_MAPPINGS_PATH, { + template, + k7Breadcrumbs: getRoleMappingBreadcrumbs, + controller($scope) { + $scope.$$postDigest(() => { + const domNode = document.getElementById('roleMappingsGridReactRoot'); + + render( + + + , + domNode + ); + + // unmount react on controller destroy + $scope.$on('$destroy', () => { + if (domNode) { + unmountComponentAtNode(domNode); + } + }); + }); + }, +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html new file mode 100644 index 0000000000000..cff3b821d132c --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html @@ -0,0 +1,3 @@ + +
+ diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/services/documentation_links.ts b/x-pack/legacy/plugins/security/public/views/management/role_mappings/services/documentation_links.ts new file mode 100644 index 0000000000000..36351f49890a1 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/role_mappings/services/documentation_links.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; + +class DocumentationLinksService { + private esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; + + public getRoleMappingDocUrl() { + return `${this.esDocBasePath}/mapping-roles.html`; + } + + public getRoleMappingAPIDocUrl() { + return `${this.esDocBasePath}/security-api-put-role-mapping.html`; + } + + public getRoleMappingTemplateDocUrl() { + return `${this.esDocBasePath}/security-api-put-role-mapping.html#_role_templates`; + } + + public getRoleMappingFieldRulesDocUrl() { + return `${this.esDocBasePath}/role-mapping-resources.html#mapping-roles-rule-field`; + } +} + +export const documentationLinks = new DocumentationLinksService(); diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 4383329fea072..5116416b527a5 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -27,8 +27,6 @@ export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; -export const DEFAULT_KBN_VERSION = 'kbnVersion'; -export const DEFAULT_TIMEZONE_BROWSER = 'timezoneBrowser'; export const DEFAULT_FROM = 'now-24h'; export const DEFAULT_TO = 'now'; export const DEFAULT_INTERVAL_PAUSE = true; diff --git a/x-pack/legacy/plugins/siem/cypress/README.md b/x-pack/legacy/plugins/siem/cypress/README.md index fb2b6cd2e3fd3..c9e0d4e18f78f 100644 --- a/x-pack/legacy/plugins/siem/cypress/README.md +++ b/x-pack/legacy/plugins/siem/cypress/README.md @@ -51,10 +51,23 @@ export const USERNAME = '[data-test-subj="loginUsername"]'; We prefer not to mock API responses in most of our smoke tests, but sometimes it's necessary because a test must assert that a specific value is rendered, and it's not possible to derive that value based on the data in the -envrionment where tests are running. +environment where tests are running. Mocked responses API from the server are located in `siem/cypress/fixtures`. +## Speeding up test execution time + +Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be +taken into consideration until another solution is implemented: + +- Don't refresh the page for every test to clean the state of it. +- Instead, group the tests that are similar in different contexts. +- For every context login only once, clean the state between tests if needed without re-loading the page. +- All tests in a spec file must be order-independent. + - If you need to reload the page to make the tests order-independent, consider to create a new context. + +Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. + ## Authentication When running tests, there are two ways to specify the credentials used to diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts index 0e3717feef7ad..6f7906d7fd791 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts @@ -19,6 +19,8 @@ export const HEADER_SUBTITLE = `${EVENTS_VIEWER_PANEL} [data-test-subj="header-p /** The inspect query modal */ export const INSPECT_MODAL = '[data-test-subj="modal-inspect-euiModal"]'; +export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]'; + /** The inspect query button that launches the inspect query modal */ export const INSPECT_QUERY = `${EVENTS_VIEWER_PANEL} [data-test-subj="inspect-icon-button"]`; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts index e3495b6a78127..405c8eb34d6fc 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts @@ -42,3 +42,7 @@ export const clickOutsideFieldsBrowser = () => { export const filterFieldsBrowser = (fieldName: string) => { cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName); }; + +export const clearFieldsBrowser = () => { + cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts deleted file mode 100644 index 8cf015619f4c1..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts +++ /dev/null @@ -1,11 +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. - */ - -/** The avatar / button that represents the logged-in Kibana user */ -export const USER_MENU = '[data-test-subj="userMenuButton"]'; - -/** Clicking this link logs out the currently logged-in Kibana user */ -export const LOGOUT_LINK = '[data-test-subj="logoutLink"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts index 85878d8225609..1450ee8dc8abf 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts @@ -10,7 +10,6 @@ import { FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, FIELDS_BROWSER_TITLE, } from '../../lib/fields_browser/selectors'; -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; import { @@ -19,6 +18,7 @@ import { filterSearchBar, } from '../../lib/events_viewer/helpers'; import { + CLOSE_MODAL, EVENTS_VIEWER_PANEL, HEADER_SUBTITLE, INSPECT_MODAL, @@ -40,166 +40,162 @@ const defaultHeadersInDefaultEcsCategory = [ ]; describe('Events Viewer', () => { - beforeEach(() => { - loginAndWaitForPage(HOSTS_PAGE); - - clickEventsTab(); - }); - - afterEach(() => { - return logout(); - }); - - it('renders the fields browser with the expected title when the Events Viewer Fields Browser button is clicked', () => { - openEventsViewerFieldsBrowser(); - - cy.get(FIELDS_BROWSER_TITLE) - .invoke('text') - .should('eq', 'Customize Columns'); - }); + context('Fields rendering', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + clickEventsTab(); + }); - it('closes the fields browser when the user clicks outside of it', () => { - openEventsViewerFieldsBrowser(); + beforeEach(() => { + openEventsViewerFieldsBrowser(); + }); - clickOutsideFieldsBrowser(); + afterEach(() => { + clickOutsideFieldsBrowser(); + cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); + }); - cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); - }); + it('renders the fields browser with the expected title when the Events Viewer Fields Browser button is clicked', () => { + cy.get(FIELDS_BROWSER_TITLE) + .invoke('text') + .should('eq', 'Customize Columns'); + }); - it('displays the `default ECS` category (by default)', () => { - openEventsViewerFieldsBrowser(); + it('displays the `default ECS` category (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) + .invoke('text') + .should('eq', 'default ECS'); + }); - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) - .invoke('text') - .should('eq', 'default ECS'); + it('displays a checked checkbox for all of the default events viewer columns that are also in the default ECS category', () => { + defaultHeadersInDefaultEcsCategory.forEach(header => + cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') + ); + }); }); - it('displays a checked checkbox for all of the default events viewer columns that are also in the default ECS category', () => { - openEventsViewerFieldsBrowser(); - - defaultHeadersInDefaultEcsCategory.forEach(header => - cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') - ); - }); + context('Events viewer query modal', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + clickEventsTab(); + }); - it('removes the message field from the timeline when the user un-checks the field', () => { - const toggleField = 'message'; + after(() => { + cy.get(CLOSE_MODAL).click(); + cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('not.exist'); + }); - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should('exist'); + it('launches the inspect query modal when the inspect button is clicked', () => { + // wait for data to load + cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) + .should('exist') + .invoke('text', { timeout: DEFAULT_TIMEOUT }) + .should('not.equal', '0'); - openEventsViewerFieldsBrowser(); + cy.get(INSPECT_QUERY, { timeout: DEFAULT_TIMEOUT }) + .should('exist') + .trigger('mousemove', { force: true }) + .click({ force: true }); - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="field-${toggleField}-checkbox"]`).uncheck({ - force: true, + cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('exist'); }); - - clickOutsideFieldsBrowser(); - - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); }); - it('filters the events by applying filter criteria from the search bar at the top of the page', () => { - const filterInput = '4bf34c1c-eaa9-46de-8921-67a4ccc49829'; // this will never match real data + context('Events viewer fields behaviour', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + clickEventsTab(); + }); - cy.get(HEADER_SUBTITLE) - .invoke('text') - .then(text1 => { - cy.get(HEADER_SUBTITLE) - .invoke('text', { timeout: DEFAULT_TIMEOUT }) - .should('not.equal', 'Showing: 0 events'); + beforeEach(() => { + openEventsViewerFieldsBrowser(); + }); - filterSearchBar(filterInput); + it('adds a field to the events viewer when the user clicks the checkbox', () => { + const filterInput = 'host.geo.c'; + const toggleField = 'host.geo.city_name'; - cy.get(HEADER_SUBTITLE) - .invoke('text') - .should(text2 => { - expect(text1).not.to.eq(text2); - }); - }); - }); + filterFieldsBrowser(filterInput); - it('adds a field to the events viewer when the user clicks the checkbox', () => { - const filterInput = 'host.geo.c'; - const toggleField = 'host.geo.city_name'; + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); - openEventsViewerFieldsBrowser(); - - filterFieldsBrowser(filterInput); + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="field-${toggleField}-checkbox"]`).check({ + force: true, + }); - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); + clickOutsideFieldsBrowser(); - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="field-${toggleField}-checkbox"]`).check({ - force: true, + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( + 'exist' + ); }); - clickOutsideFieldsBrowser(); + it('resets all fields in the events viewer when `Reset Fields` is clicked', () => { + const filterInput = 'host.geo.c'; + const toggleField = 'host.geo.country_name'; - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should('exist'); - }); + filterFieldsBrowser(filterInput); - it('loads more events when the load more button is clicked', () => { - cy.get(LOCAL_EVENTS_COUNT, { timeout: DEFAULT_TIMEOUT }) - .invoke('text') - .then(text1 => { - cy.get(LOCAL_EVENTS_COUNT) - .invoke('text') - .should('equal', '25'); - - cy.get(LOAD_MORE).click({ force: true }); - - cy.get(LOCAL_EVENTS_COUNT) - .invoke('text') - .should(text2 => { - expect(text1).not.to.eq(text2); - }); - }); - }); + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); - it('launches the inspect query modal when the inspect button is clicked', () => { - // wait for data to load - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) - .should('exist') - .invoke('text', { timeout: DEFAULT_TIMEOUT }) - .should('not.equal', '0'); + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="field-${toggleField}-checkbox"]`).check({ + force: true, + }); - cy.get(INSPECT_QUERY, { timeout: DEFAULT_TIMEOUT }) - .should('exist') - .trigger('mousemove', { force: true }) - .click({ force: true }); + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="reset-fields"]`).click({ force: true }); - cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('exist'); + cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); + }); }); - it('resets all fields in the events viewer when `Reset Fields` is clicked', () => { - const filterInput = 'host.geo.c'; - const toggleField = 'host.geo.city_name'; - - openEventsViewerFieldsBrowser(); - - filterFieldsBrowser(filterInput); - - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); - - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="field-${toggleField}-checkbox"]`).check({ - force: true, + context('Events behaviour', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + clickEventsTab(); }); - clickOutsideFieldsBrowser(); + it('filters the events by applying filter criteria from the search bar at the top of the page', () => { + const filterInput = '4bf34c1c-eaa9-46de-8921-67a4ccc49829'; // this will never match real data - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should('exist'); + cy.get(HEADER_SUBTITLE) + .invoke('text') + .then(text1 => { + cy.get(HEADER_SUBTITLE) + .invoke('text', { timeout: DEFAULT_TIMEOUT }) + .should('not.equal', 'Showing: 0 events'); - openEventsViewerFieldsBrowser(); + filterSearchBar(filterInput); - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="reset-fields"]`).click({ force: true }); + cy.get(HEADER_SUBTITLE) + .invoke('text') + .should(text2 => { + expect(text1).not.to.eq(text2); + }); + }); + }); - cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); + it('loads more events when the load more button is clicked', () => { + cy.get(LOCAL_EVENTS_COUNT, { timeout: DEFAULT_TIMEOUT }) + .invoke('text') + .then(text1 => { + cy.get(LOCAL_EVENTS_COUNT) + .invoke('text') + .should('equal', '25'); + + cy.get(LOAD_MORE).click({ force: true }); + + cy.get(LOCAL_EVENTS_COUNT) + .invoke('text') + .should(text2 => { + expect(text1).not.to.eq(text2); + }); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index dfc5e10893ebb..d1289732b6d7d 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -6,6 +6,7 @@ import { drag, drop } from '../../lib/drag_n_drop/helpers'; import { + clearFieldsBrowser, clickOutsideFieldsBrowser, openTimelineFieldsBrowser, populateTimeline, @@ -22,7 +23,6 @@ import { FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_TITLE, } from '../../lib/fields_browser/selectors'; -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; @@ -38,237 +38,200 @@ const defaultHeaders = [ ]; describe('Fields Browser', () => { - beforeEach(() => { - loginAndWaitForPage(HOSTS_PAGE); - }); - - afterEach(() => { - return logout(); - }); - - it('renders the fields browser with the expected title when the Fields button is clicked', () => { - populateTimeline(); - - openTimelineFieldsBrowser(); - - cy.get(FIELDS_BROWSER_TITLE) - .invoke('text') - .should('eq', 'Customize Columns'); - }); - - it('closes the fields browser when the user clicks outside of it', () => { - populateTimeline(); - - openTimelineFieldsBrowser(); - - clickOutsideFieldsBrowser(); - - cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); - }); - - it('displays the `default ECS` category (by default)', () => { - populateTimeline(); - - openTimelineFieldsBrowser(); - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) - .invoke('text') - .should('eq', 'default ECS'); - }); - - it('the `defaultECS` (selected) category count matches the default timeline header count', () => { - populateTimeline(); - - openTimelineFieldsBrowser(); - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT) - .invoke('text') - .should('eq', `${defaultHeaders.length}`); - }); - - it('displays a checked checkbox for all of the default timeline columns', () => { - populateTimeline(); - - openTimelineFieldsBrowser(); - - defaultHeaders.forEach(header => - cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') - ); - }); + context('Fields Browser rendering', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + populateTimeline(); + openTimelineFieldsBrowser(); + }); - it('removes the message field from the timeline when the user un-checks the field', () => { - const toggleField = 'message'; + afterEach(() => { + clearFieldsBrowser(); + }); - populateTimeline(); + it('renders the fields browser with the expected title when the Fields button is clicked', () => { + cy.get(FIELDS_BROWSER_TITLE) + .invoke('text') + .should('eq', 'Customize Columns'); + }); - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'exist' - ); + it('displays the `default ECS` category (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) + .invoke('text') + .should('eq', 'default ECS'); + }); - openTimelineFieldsBrowser(); + it('the `defaultECS` (selected) category count matches the default timeline header count', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT) + .invoke('text') + .should('eq', `${defaultHeaders.length}`); + }); - cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).uncheck({ - force: true, + it('displays a checked checkbox for all of the default timeline columns', () => { + defaultHeaders.forEach(header => + cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') + ); }); - clickOutsideFieldsBrowser(); + it('displays the expected count of categories that match the filter input', () => { + const filterInput = 'host.mac'; - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); - }); + filterFieldsBrowser(filterInput); - it('displays the expected count of categories that match the filter input', () => { - const filterInput = 'host.mac'; + cy.get(FIELDS_BROWSER_CATEGORIES_COUNT) + .invoke('text') + .should('eq', '2 categories'); + }); - populateTimeline(); + it('displays a search results label with the expected count of fields matching the filter input', () => { + const filterInput = 'host.mac'; + + filterFieldsBrowser(filterInput); + + cy.get(FIELDS_BROWSER_FILTER_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + 'not.have.class', + 'euiFieldSearch-isLoading' + ); + + cy.get(FIELDS_BROWSER_HOST_CATEGORIES_COUNT) + .invoke('text') + .then(hostCategoriesCount => { + cy.get(FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT) + .invoke('text') + .then(systemCategoriesCount => { + cy.get(FIELDS_BROWSER_FIELDS_COUNT) + .invoke('text') + .should('eq', `${+hostCategoriesCount + +systemCategoriesCount} fields`); + }); + }); + }); - openTimelineFieldsBrowser(); + it('displays a count of only the fields in the selected category that match the filter input', () => { + const filterInput = 'host.geo.c'; - filterFieldsBrowser(filterInput); + filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_CATEGORIES_COUNT) - .invoke('text') - .should('eq', '2 categories'); + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT) + .invoke('text') + .should('eq', '4'); + }); }); - it('displays a search results label with the expected count of fields matching the filter input', () => { - const filterInput = 'host.mac'; - - populateTimeline(); + context('Editing the timeline', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + populateTimeline(); + openTimelineFieldsBrowser(); + }); - openTimelineFieldsBrowser(); + afterEach(() => { + openTimelineFieldsBrowser(); + clearFieldsBrowser(); + }); - filterFieldsBrowser(filterInput); + it('removes the message field from the timeline when the user un-checks the field', () => { + const toggleField = 'message'; - cy.get(FIELDS_BROWSER_FILTER_INPUT, { timeout: DEFAULT_TIMEOUT }).should( - 'not.have.class', - 'euiFieldSearch-isLoading' - ); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'exist' + ); - cy.get(FIELDS_BROWSER_HOST_CATEGORIES_COUNT) - .invoke('text') - .then(hostCategoriesCount => { - cy.get(FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT) - .invoke('text') - .then(systemCategoriesCount => { - cy.get(FIELDS_BROWSER_FIELDS_COUNT) - .invoke('text') - .should('eq', `${+hostCategoriesCount + +systemCategoriesCount} fields`); - }); + cy.get( + `[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]` + ).uncheck({ + force: true, }); - }); - - it('selects a search results label with the expected count of categories matching the filter input', () => { - const category = 'host'; - - populateTimeline(); - openTimelineFieldsBrowser(); - - filterFieldsBrowser(`${category}.`); - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) - .invoke('text') - .should('eq', category); - }); + clickOutsideFieldsBrowser(); - it('displays a count of only the fields in the selected category that match the filter input', () => { - const filterInput = 'host.geo.c'; + cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); - populateTimeline(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); + }); - openTimelineFieldsBrowser(); + it('selects a search results label with the expected count of categories matching the filter input', () => { + const category = 'host'; - filterFieldsBrowser(filterInput); + filterFieldsBrowser(`${category}.`); - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT) - .invoke('text') - .should('eq', '4'); - }); + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) + .invoke('text') + .should('eq', category); + }); - it('adds a field to the timeline when the user clicks the checkbox', () => { - const filterInput = 'host.geo.c'; - const toggleField = 'host.geo.city_name'; + it('adds a field to the timeline when the user clicks the checkbox', () => { + const filterInput = 'host.geo.c'; + const toggleField = 'host.geo.city_name'; - populateTimeline(); + filterFieldsBrowser(filterInput); - openTimelineFieldsBrowser(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); - filterFieldsBrowser(filterInput); + cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check({ + force: true, + }); - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); + clickOutsideFieldsBrowser(); - cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check({ - force: true, + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`, { + timeout: DEFAULT_TIMEOUT, + }).should('exist'); }); - clickOutsideFieldsBrowser(); + it('adds a field to the timeline when the user drags and drops a field', () => { + const filterInput = 'host.geo.c'; + const toggleField = 'host.geo.country_name'; - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'exist' - ); - }); - - it('adds a field to the timeline when the user drags and drops a field', () => { - const filterInput = 'host.geo.c'; - const toggleField = 'host.geo.city_name'; + filterFieldsBrowser(filterInput); - populateTimeline(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); - openTimelineFieldsBrowser(); + cy.get( + `[data-test-subj="timeline"] [data-test-subj="field-name-${toggleField}"]` + ).then(field => drag(field)); - filterFieldsBrowser(filterInput); + cy.get(`[data-test-subj="timeline"] [data-test-subj="headers-group"]`).then(headersDropArea => + drop(headersDropArea) + ); - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`, { + timeout: DEFAULT_TIMEOUT, + }).should('exist'); + }); - cy.get(`[data-test-subj="timeline"] [data-test-subj="field-name-${toggleField}"]`).then(field => - drag(field) - ); + it('resets all fields in the timeline when `Reset Fields` is clicked', () => { + const filterInput = 'host.geo.c'; + const toggleField = 'host.geo.continent_name'; - cy.get(`[data-test-subj="timeline"] [data-test-subj="headers-group"]`).then(headersDropArea => - drop(headersDropArea) - ); + filterFieldsBrowser(filterInput); - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`, { - timeout: DEFAULT_TIMEOUT, - }).should('exist'); - }); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); - it('resets all fields in the timeline when `Reset Fields` is clicked', () => { - const filterInput = 'host.geo.c'; - const toggleField = 'host.geo.city_name'; + cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check({ + force: true, + }); - populateTimeline(); + clickOutsideFieldsBrowser(); - openTimelineFieldsBrowser(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'exist' + ); - filterFieldsBrowser(filterInput); + openTimelineFieldsBrowser(); - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); + cy.get('[data-test-subj="timeline"] [data-test-subj="reset-fields"]').click({ force: true }); - cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check({ - force: true, + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( + 'not.exist' + ); }); - - clickOutsideFieldsBrowser(); - - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'exist' - ); - - openTimelineFieldsBrowser(); - - cy.get('[data-test-subj="timeline"] [data-test-subj="reset-fields"]').click({ force: true }); - - cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( - 'not.exist' - ); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts index 54207966fd36f..ee25705a83989 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { INSPECT_BUTTON_ICON, @@ -18,9 +17,6 @@ import { executeKQL, hostExistsQuery, toggleTimelineVisibility } from '../../lib describe('Inspect', () => { describe('Hosts and network stats and tables', () => { - afterEach(() => { - return logout(); - }); INSPECT_BUTTONS_IN_SIEM.map(table => it(`inspects the ${table.title}`, () => { loginAndWaitForPage(table.url); @@ -36,10 +32,6 @@ describe('Inspect', () => { }); describe('Timeline', () => { - afterEach(() => { - return logout(); - }); - it('inspects the timeline', () => { loginAndWaitForPage(HOSTS_PAGE); toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index 4c29c081b3e69..afeb8c3c13a4f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { mlNetworkSingleIpNullKqlQuery, mlNetworkSingleIpKqlQuery, @@ -24,10 +23,6 @@ import { loginAndWaitForPage } from '../../lib/util/helpers'; import { KQL_INPUT } from '../../lib/url_state'; describe('ml conditional links', () => { - afterEach(() => { - return logout(); - }); - it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts index f4beba7cbb72d..a549b5eec2e7c 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; -import { OVERVIEW_PAGE, TIMELINES_PAGE } from '../../lib/urls'; +import { TIMELINES_PAGE } from '../../lib/urls'; import { NAVIGATION_HOSTS, NAVIGATION_NETWORK, @@ -15,37 +14,27 @@ import { import { loginAndWaitForPage } from '../../lib/util/helpers'; describe('top-level navigation common to all pages in the SIEM app', () => { - afterEach(() => { - return logout(); + before(() => { + loginAndWaitForPage(TIMELINES_PAGE); }); - it('navigates to the Overview page', () => { - loginAndWaitForPage(TIMELINES_PAGE); - cy.get(NAVIGATION_OVERVIEW).click({ force: true }); - cy.url().should('include', '/siem#/overview'); }); it('navigates to the Hosts page', () => { - loginAndWaitForPage(TIMELINES_PAGE); - cy.get(NAVIGATION_HOSTS).click({ force: true }); cy.url().should('include', '/siem#/hosts'); }); it('navigates to the Network page', () => { - loginAndWaitForPage(TIMELINES_PAGE); - cy.get(NAVIGATION_NETWORK).click({ force: true }); cy.url().should('include', '/siem#/network'); }); it('navigates to the Timelines page', () => { - loginAndWaitForPage(OVERVIEW_PAGE); - cy.get(NAVIGATION_TIMELINES).click({ force: true }); cy.url().should('include', '/siem#/timelines'); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts index 2ea8b5e8bc5ce..4ef3eb67cafc9 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { OVERVIEW_PAGE } from '../../lib/urls'; import { clearFetch, stubApi } from '../../lib/fixtures/helpers'; import { HOST_STATS, NETWORK_STATS, STAT_AUDITD } from '../../lib/overview/selectors'; @@ -17,10 +16,6 @@ describe('Overview Page', () => { loginAndWaitForPage(OVERVIEW_PAGE); }); - afterEach(() => { - return logout(); - }); - it('Host and Network stats render with correct values', () => { cy.get(STAT_AUDITD.domId); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts index ebd0ad0125efb..3853e703a7c07 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { HOSTS_PAGE_TAB_URLS } from '../../lib/urls'; import { AUTHENTICATIONS_TABLE, @@ -19,13 +18,16 @@ import { import { DEFAULT_TIMEOUT, loginAndWaitForPage, waitForTableLoad } from '../../lib/util/helpers'; describe('Pagination', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); + waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); + }); + afterEach(() => { - return logout(); + cy.get(getPageButtonSelector(0)).click({ force: true }); }); it('pagination updates results and page number', () => { - loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); cy.get(getDraggableField('process.name')) @@ -47,8 +49,6 @@ describe('Pagination', () => { }); it('pagination keeps track of page results when tabs change', () => { - loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); let thirdPageResult: string; cy.get(getPageButtonSelector(2)).click({ force: true }); @@ -83,7 +83,6 @@ describe('Pagination', () => { * when we figure out a way to really mock the data, we should come back to it */ it('pagination resets results and page number to first page when refresh is clicked', () => { - loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); cy.get(NUMBERED_PAGINATION, { timeout: DEFAULT_TIMEOUT }); cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); // let firstResult: string; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 236d5a53481b7..824e403185238 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { TIMELINE_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS, @@ -22,10 +21,6 @@ describe('timeline data providers', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { waitForAllHostsWidget(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts index c1c35e497d081..5b0ac03ae87dc 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON, @@ -21,10 +20,6 @@ describe('timeline flyout button', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('toggles open the timeline', () => { toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts index 0c9aed33d47ad..9f21b4e3d53a1 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { assertAtLeastOneEventMatchesSearch, executeKQL, @@ -19,10 +18,6 @@ describe('timeline search or filter KQL bar', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('executes a KQL query', () => { toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 8197f77db9a08..9a915b0e77d44 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -6,7 +6,6 @@ import { drag, drop } from '../../lib/drag_n_drop/helpers'; import { populateTimeline } from '../../lib/fields_browser/helpers'; -import { logout } from '../../lib/logout'; import { toggleFirstTimelineEventDetails } from '../../lib/timeline/helpers'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; @@ -16,10 +15,6 @@ describe('toggle column in timeline', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - const timestampField = '@timestamp'; const idField = '_id'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index dba5099a93c5a..33ee2cb1cb302 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { ABSOLUTE_DATE_RANGE, DATE_PICKER_ABSOLUTE_INPUT, @@ -33,10 +32,6 @@ import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; import { NAVIGATION_HOSTS_ALL_HOSTS, NAVIGATION_HOSTS_ANOMALIES } from '../../lib/hosts/selectors'; describe('url state', () => { - afterEach(() => { - return logout(); - }); - it('sets the global start and end dates from the url', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( diff --git a/x-pack/legacy/plugins/siem/cypress/support/index.js b/x-pack/legacy/plugins/siem/cypress/support/index.js index b20865149d02f..9b86c2ffa94c6 100644 --- a/x-pack/legacy/plugins/siem/cypress/support/index.js +++ b/x-pack/legacy/plugins/siem/cypress/support/index.js @@ -22,5 +22,9 @@ // Import commands.js using ES2015 syntax: import './commands'; +Cypress.Cookies.defaults({ + whitelist: 'sid', +}); + // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js index a4a9532f0e8e4..7d76b1dd921aa 100644 --- a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js @@ -24,9 +24,7 @@ run( // We can only care about SIEM code, we should not be penalyze for others if (circularFound.filter(cf => cf.includes('siem')).length !== 0) { throw createFailError( - 'SIEM circular dependencies of imports has been found:' + - '\n - ' + - circularFound.join('\n - ') + `SIEM circular dependencies of imports has been found:\n - ${circularFound.join('\n - ')}` ); } else { log.success('No circular deps 👍'); diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index cf9fffc6a1455..c5038626fdfc2 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -9,7 +9,7 @@ import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { plugin } from './server'; import { savedObjectMappings } from './server/saved_objects'; @@ -43,7 +43,7 @@ export const siem = (kibana: any) => { description: i18n.translate('xpack.siem.securityDescription', { defaultMessage: 'Explore your SIEM App', }), - main: 'plugins/siem/app', + main: 'plugins/siem/legacy', euiIconType: 'securityAnalyticsApp', title: APP_NAME, listed: false, diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index bf5d6d3a3089c..558ac013e5963 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^11.0.3" + "@types/react-beautiful-dnd": "^11.0.4" }, "dependencies": { "lodash": "^4.17.15", diff --git a/x-pack/legacy/plugins/siem/public/app/app.tsx b/x-pack/legacy/plugins/siem/public/app/app.tsx new file mode 100644 index 0000000000000..5f9199735d8c0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/app/app.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createHashHistory, History } from 'history'; +import React, { memo, useMemo, FC } from 'react'; +import { ApolloProvider } from 'react-apollo'; +import { Store } from 'redux'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { ThemeProvider } from 'styled-components'; + +import { EuiErrorBoundary } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { BehaviorSubject } from 'rxjs'; +import { pluck } from 'rxjs/operators'; +import { I18nContext } from 'ui/i18n'; + +import { KibanaContextProvider, useUiSetting$ } from '../lib/kibana'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +import { DEFAULT_DARK_MODE } from '../../common/constants'; +import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; +import { compose } from '../lib/compose/kibana_compose'; +import { AppFrontendLibs, AppApolloClient } from '../lib/lib'; +import { CoreStart, StartPlugins } from '../plugin'; +import { PageRouter } from '../routes'; +import { createStore } from '../store'; +import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; +import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabilities_provider'; + +import { ApolloClientContext } from '../utils/apollo_context'; + +interface AppPluginRootComponentProps { + apolloClient: AppApolloClient; + history: History; + store: Store; + theme: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +const AppPluginRootComponent: React.FC = ({ + theme, + store, + apolloClient, + history, +}) => ( + + + + + + + + + + + + + + + + + + + +); + +const AppPluginRoot = memo(AppPluginRootComponent); + +const StartAppComponent: FC = libs => { + const history = createHashHistory(); + const libs$ = new BehaviorSubject(libs); + const store = createStore(undefined, libs$.pipe(pluck('apolloClient'))); + const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); + const theme = useMemo( + () => ({ + eui: darkMode ? euiDarkVars : euiLightVars, + darkMode, + }), + [darkMode] + ); + + return ( + + ); +}; + +const StartApp = memo(StartAppComponent); + +interface SiemAppComponentProps { + core: CoreStart; + plugins: StartPlugins; +} + +const SiemAppComponent: React.FC = ({ core, plugins }) => ( + + + +); + +export const SiemApp = memo(SiemAppComponent); diff --git a/x-pack/legacy/plugins/siem/public/app/index.tsx b/x-pack/legacy/plugins/siem/public/app/index.tsx new file mode 100644 index 0000000000000..01175a98d1e44 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/app/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { CoreStart, StartPlugins, AppMountParameters } from '../plugin'; +import { SiemApp } from './app'; + +export const renderApp = ( + core: CoreStart, + plugins: StartPlugins, + { element }: AppMountParameters +) => { + render(, element); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts deleted file mode 100644 index 0cc5c5584e1b7..0000000000000 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; -import { Plugin } from './plugin'; - -const { data, embeddable, inspector, uiActions } = npStart.plugins; -const startPlugins = { data, embeddable, inspector, uiActions }; - -new Plugin( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { opaqueId: Symbol('siem'), env: {} as any, config: { get: () => ({} as any) } }, - chrome -).start(npStart.core, startPlugins); diff --git a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx deleted file mode 100644 index aa42504e07635..0000000000000 --- a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render } from 'react-dom'; -import { LegacyCoreStart, PluginInitializerContext } from 'src/core/public'; -import { PluginsStart } from 'ui/new_platform/new_platform'; -import { Chrome } from 'ui/chrome'; - -import { DEFAULT_KBN_VERSION, DEFAULT_TIMEZONE_BROWSER } from '../../common/constants'; -import { SiemApp } from './start_app'; -import template from './template.html'; - -export const ROOT_ELEMENT_ID = 'react-siem-root'; - -export type StartCore = LegacyCoreStart; -export type StartPlugins = Required< - Pick ->; -export type StartServices = StartCore & StartPlugins; - -export class Plugin { - constructor( - // @ts-ignore this is added to satisfy the New Platform typing constraint, - // but we're not leveraging any of its functionality yet. - private readonly initializerContext: PluginInitializerContext, - private readonly chrome: Chrome - ) { - this.chrome = chrome; - } - - public start(core: StartCore, plugins: StartPlugins) { - // TODO(rylnd): These are unknown by uiSettings by default - core.uiSettings.set(DEFAULT_KBN_VERSION, '8.0.0'); - core.uiSettings.set(DEFAULT_TIMEZONE_BROWSER, 'UTC'); - - // @ts-ignore improper type description - this.chrome.setRootTemplate(template); - const checkForRoot = () => { - return new Promise(resolve => { - const ready = !!document.getElementById(ROOT_ELEMENT_ID); - if (ready) { - resolve(); - } else { - setTimeout(() => resolve(checkForRoot()), 10); - } - }); - }; - checkForRoot().then(() => { - const node = document.getElementById(ROOT_ELEMENT_ID); - if (node) { - render(, node); - } - }); - } -} diff --git a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx deleted file mode 100644 index 54180b51fe039..0000000000000 --- a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx +++ /dev/null @@ -1,87 +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 { createHashHistory } from 'history'; -import React, { memo, FC } from 'react'; -import { ApolloProvider } from 'react-apollo'; -import { Provider as ReduxStoreProvider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; - -import { EuiErrorBoundary } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { BehaviorSubject } from 'rxjs'; -import { pluck } from 'rxjs/operators'; -import { I18nContext } from 'ui/i18n'; - -import { KibanaContextProvider, useUiSetting$ } from '../lib/kibana'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; - -import { DEFAULT_DARK_MODE } from '../../common/constants'; -import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; -import { compose } from '../lib/compose/kibana_compose'; -import { AppFrontendLibs } from '../lib/lib'; -import { StartCore, StartPlugins } from './plugin'; -import { PageRouter } from '../routes'; -import { createStore } from '../store'; -import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; -import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabilities_provider'; - -import { ApolloClientContext } from '../utils/apollo_context'; - -const StartApp: FC = memo(libs => { - const history = createHashHistory(); - - const libs$ = new BehaviorSubject(libs); - - const store = createStore(undefined, libs$.pipe(pluck('apolloClient'))); - - const AppPluginRoot = memo(() => { - const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); - return ( - - - - - - - ({ - eui: darkMode ? euiDarkVars : euiLightVars, - darkMode, - })} - > - - - - - - - - - - - - - ); - }); - return ; -}); - -export const ROOT_ELEMENT_ID = 'react-siem-root'; - -export const SiemApp = memo<{ core: StartCore; plugins: StartPlugins }>(({ core, plugins }) => ( - - - -)); diff --git a/x-pack/legacy/plugins/siem/public/apps/template.html b/x-pack/legacy/plugins/siem/public/apps/template.html deleted file mode 100644 index 9f757b25ccecb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/apps/template.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx index 8fa4f3625c34f..179474ee6e9d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx @@ -51,33 +51,31 @@ const defaultAlertsFilters: esFilters.Filter[] = [ }, ]; -export const AlertsTable = React.memo( - ({ - endDate, - startDate, - pageFilters = [], - }: { - endDate: number; - startDate: number; - pageFilters?: esFilters.Filter[]; - }) => { - const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); - return ( - ({ - documentType: i18n.ALERTS_DOCUMENT_TYPE, - footerText: i18n.TOTAL_COUNT_OF_ALERTS, - title: i18n.ALERTS_TABLE_TITLE, - }), - [] - )} - /> - ); - } -); +interface Props { + endDate: number; + startDate: number; + pageFilters?: esFilters.Filter[]; +} + +const AlertsTableComponent: React.FC = ({ endDate, startDate, pageFilters = [] }) => { + const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); + return ( + ({ + documentType: i18n.ALERTS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_ALERTS, + title: i18n.ALERTS_TABLE_TITLE, + }), + [] + )} + /> + ); +}; + +export const AlertsTable = React.memo(AlertsTableComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx index be449e3d422d9..2fb270c284000 100644 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx @@ -5,7 +5,7 @@ */ import { EuiBadge } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx index 10d3c899562e8..5404a1ac43844 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; @@ -20,7 +19,7 @@ describe('arrows', () => { ); - expect(toJson(wrapper.find('ArrowBody'))).toMatchSnapshot(); + expect(wrapper.find('ArrowBody')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx index dfc7645c564d2..97b5eb04ac7bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx @@ -5,7 +5,7 @@ */ import { EuiIcon } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; /** Renders the body (non-pointy part) of an arrow */ diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx index 77a7296e368cf..27e87d25e286f 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx @@ -7,9 +7,8 @@ import { EuiFieldSearch } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { noop } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; @@ -117,7 +116,7 @@ describe('Autocomplete', () => { value={''} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it is rendering with placeholder', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx index 6816bff24f1cd..d99a909efad10 100644 --- a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; import { PreferenceFormattedBytes } from '../formatted_bytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.tsx b/x-pack/legacy/plugins/siem/public/components/bytes/index.tsx index fbe83623211b1..94c6ecba68be5 100644 --- a/x-pack/legacy/plugins/siem/public/components/bytes/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/bytes/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DefaultDraggable } from '../draggables'; import { PreferenceFormattedBytes } from '../formatted_bytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx index b0c165fedfffc..9cd0af062c54a 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx index f8db7d754aab1..181d92dce06f9 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx @@ -5,7 +5,7 @@ */ import { EuiText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx index 2b99efc05fd8c..ac283790671d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx @@ -5,7 +5,7 @@ */ import { ShallowWrapper, shallow } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { AreaChartBaseComponent, AreaChartComponent } from './areachart'; import { ChartSeriesData } from './common'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index ba07a3f3436d9..71f22efadc6ed 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -18,6 +18,7 @@ import { import { getOr, get, isNull, isNumber } from 'lodash/fp'; import { AutoSizer } from '../auto_sizer'; import { ChartPlaceHolder } from './chart_place_holder'; +import { useTimeZone } from '../../hooks'; import { chartDefaultSettings, ChartSeriesConfigs, @@ -26,7 +27,6 @@ import { getChartWidth, WrappedByAutoSizer, useTheme, - useBrowserTimeZone, } from './common'; // custom series styles: https://ela.st/areachart-styling @@ -71,7 +71,7 @@ export const AreaChartBaseComponent = ({ configs?: ChartSeriesConfigs | undefined; }) => { const theme = useTheme(); - const timeZone = useBrowserTimeZone(); + const timeZone = useTimeZone(); const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs); const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs); const xAxisId = `group-${data[0].key}-x`; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index 506b1ceb5ed83..ac9c4d591232a 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -5,7 +5,7 @@ */ import { shallow, ShallowWrapper } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index db84d7dbd2c18..415cbeb7c2440 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; +import { useTimeZone } from '../../hooks'; import { AutoSizer } from '../auto_sizer'; import { ChartPlaceHolder } from './chart_place_holder'; import { @@ -17,7 +18,6 @@ import { getChartHeight, getChartWidth, WrappedByAutoSizer, - useBrowserTimeZone, useTheme, } from './common'; @@ -44,7 +44,7 @@ export const BarChartBaseComponent = ({ configs?: ChartSeriesConfigs | undefined; }) => { const theme = useTheme(); - const timeZone = useBrowserTimeZone(); + const timeZone = useTimeZone(); const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs); const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs); const tickSize = getOr(0, 'configs.axis.tickSize', chartConfigs); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index a4be390019916..78cce72f0a0d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -15,10 +15,9 @@ import { SettingsSpecProps, TickFormatter, } from '@elastic/charts'; -import moment from 'moment-timezone'; import styled from 'styled-components'; import { useUiSetting } from '../../lib/kibana'; -import { DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE } from '../../../common/constants'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; export const defaultChartHeight = '100%'; export const defaultChartWidth = '100%'; @@ -108,11 +107,6 @@ export const chartDefaultSettings = { debug: false, }; -export const useBrowserTimeZone = () => { - const kibanaTimezone = useUiSetting(DEFAULT_DATE_FORMAT_TZ); - return kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; -}; - export const getChartHeight = (customHeight?: number, autoSizerHeight?: number): string => { const height = customHeight || autoSizerHeight; return height ? `${height}px` : defaultChartHeight; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index a5eac381f9215..eae0fc4ff422b 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -6,7 +6,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../../mock'; @@ -44,7 +43,7 @@ describe('UtilityBar', () => { ); - expect(toJson(wrapper.find('UtilityBar'))).toMatchSnapshot(); + expect(wrapper.find('UtilityBar')).toMatchSnapshot(); }); test('it applies border styles when border is true', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index 2610fb44532f5..2a8a71955a986 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -5,7 +5,6 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../../mock'; @@ -19,7 +18,7 @@ describe('UtilityBarAction', () => { ); - expect(toJson(wrapper.find('UtilityBarAction'))).toMatchSnapshot(); + expect(wrapper.find('UtilityBarAction')).toMatchSnapshot(); }); test('it renders a popover', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx index 59ef7021d4049..e18e7d5e0b524 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../../mock'; @@ -21,6 +20,6 @@ describe('UtilityBarGroup', () => { ); - expect(toJson(wrapper.find('UtilityBarGroup'))).toMatchSnapshot(); + expect(wrapper.find('UtilityBarGroup')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx index baa4331ced8f8..f849fa4b4ee46 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../../mock'; @@ -23,6 +22,6 @@ describe('UtilityBarSection', () => { ); - expect(toJson(wrapper.find('UtilityBarSection'))).toMatchSnapshot(); + expect(wrapper.find('UtilityBarSection')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx index 794f207fd88e3..230dd80b1a86b 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../../mock'; @@ -19,6 +18,6 @@ describe('UtilityBarText', () => { ); - expect(toJson(wrapper.find('UtilityBarText'))).toMatchSnapshot(); + expect(wrapper.find('UtilityBarText')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx index 9295e055f918d..ad1e63dbd7e6a 100644 --- a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { NetworkDirectionEcs } from '../../graphql/types'; import { DraggableBadge } from '../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx index 1a8af9d99193a..9e8bde8d9ff92 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; @@ -28,7 +27,7 @@ describe('DragDropContextWrapper', () => { ); - expect(toJson(wrapper.find('DragDropContextWrapper'))).toMatchSnapshot(); + expect(wrapper.find('DragDropContextWrapper')).toMatchSnapshot(); }); test('it renders the children', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 4b546bca1f72e..e846c923c5cbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; @@ -33,7 +32,7 @@ describe('DraggableWrapper', () => { ); - expect(toJson(wrapper.find('DraggableWrapper'))).toMatchSnapshot(); + expect(wrapper.find('DraggableWrapper')).toMatchSnapshot(); }); test('it renders the children passed to the render prop', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 4faf02ead3fe1..9672097713a9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -13,15 +13,15 @@ import { Droppable, } from 'react-beautiful-dnd'; import { connect } from 'react-redux'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; import { EuiPortal } from '@elastic/eui'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; -import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../timeline/helpers'; import { TruncatableText } from '../truncatable_text'; import { getDraggableId, getDroppableId } from './helpers'; +import { ProviderContainer } from './provider_container'; // As right now, we do not know what we want there, we will keep it as a placeholder export const DragEffects = styled.div``; @@ -42,143 +42,12 @@ const Wrapper = styled.div` Wrapper.displayName = 'Wrapper'; -const ProviderContainer = styled.div<{ isDragging: boolean }>` - &, - &::before, - &::after { - transition: background ${({ theme }) => theme.eui.euiAnimSpeedFast} ease, - color ${({ theme }) => theme.eui.euiAnimSpeedFast} ease; +const ProviderContentWrapper = styled.span` + > span.euiToolTipAnchor { + display: block; /* allow EuiTooltip content to be truncatable */ } - - ${({ isDragging }) => - !isDragging && - css` - & { - border-radius: 2px; - padding: 0 4px 0 8px; - position: relative; - z-index: ${({ theme }) => theme.eui.euiZLevel0} !important; - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorMediumShade} 25%, - transparent 25% - ), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorMediumShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorMediumShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorMediumShade} 75% - ); - background-position: 0 0, 1px 0, 1px -1px, 0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; - } - } - - &:hover { - &, - & .euiBadge, - & .euiBadge__text { - cursor: move; /* Fallback for IE11 */ - cursor: grab; - } - } - - .${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &, - tr:hover & { - background-color: ${({ theme }) => theme.eui.euiColorLightShade}; - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorDarkShade} 25%, - transparent 25% - ), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorDarkShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorDarkShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorDarkShade} 75% - ); - } - } - - &:hover, - &:focus, - .${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &:hover, - .${STATEFUL_EVENT_CSS_CLASS_NAME}:focus &:focus, - tr:hover &:hover, - tr:hover &:focus { - background-color: ${({ theme }) => theme.eui.euiColorPrimary}; - - &, - & a, - & a:hover { - color: ${({ theme }) => theme.eui.euiColorEmptyShade}; - } - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorEmptyShade} 25%, - transparent 25% - ), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorEmptyShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorEmptyShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorEmptyShade} 75% - ); - } - } - `} - - ${({ isDragging }) => - isDragging && - css` - & { - z-index: 9999 !important; - } - `} `; -ProviderContainer.displayName = 'ProviderContainer'; - interface OwnProps { dataProvider: DataProvider; inline?: boolean; @@ -244,9 +113,11 @@ const DraggableWrapperComponent = React.memo( {render(dataProvider, provided, snapshot)} ) : ( - + {render(dataProvider, provided, snapshot)} - + )} diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx index 056669673bb9e..bd2f01721290f 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; @@ -33,7 +32,7 @@ describe('DroppableWrapper', () => { ); - expect(toJson(wrapper.find('DroppableWrapper'))).toMatchSnapshot(); + expect(wrapper.find('DroppableWrapper')).toMatchSnapshot(); }); test('it renders the children when a render prop is not provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index c660ac6adaa71..821ef9be10e8d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -5,7 +5,7 @@ */ import { rgba } from 'polished'; -import * as React from 'react'; +import React from 'react'; import { Droppable } from 'react-beautiful-dnd'; import styled from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx new file mode 100644 index 0000000000000..c1f029086aa35 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled, { css } from 'styled-components'; +import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../timeline/helpers'; + +interface ProviderContainerProps { + isDragging: boolean; +} + +const ProviderContainerComponent = styled.div` + &, + &::before, + &::after { + transition: background ${({ theme }) => theme.eui.euiAnimSpeedFast} ease, + color ${({ theme }) => theme.eui.euiAnimSpeedFast} ease; + } + + ${({ isDragging }) => + !isDragging && + css` + & { + border-radius: 2px; + padding: 0 4px 0 8px; + position: relative; + z-index: ${({ theme }) => theme.eui.euiZLevel0} !important; + + &::before { + background-image: linear-gradient( + 135deg, + ${({ theme }) => theme.eui.euiColorMediumShade} 25%, + transparent 25% + ), + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorMediumShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorMediumShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorMediumShade} 75% + ); + background-position: 0 0, 1px 0, 1px -1px, 0px 1px; + background-size: 2px 2px; + bottom: 2px; + content: ''; + display: block; + left: 2px; + position: absolute; + top: 2px; + width: 4px; + } + } + + &:hover { + &, + & .euiBadge, + & .euiBadge__text { + cursor: move; /* Fallback for IE11 */ + cursor: grab; + } + } + + .${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &, + tr:hover & { + background-color: ${({ theme }) => theme.eui.euiColorLightShade}; + + &::before { + background-image: linear-gradient( + 135deg, + ${({ theme }) => theme.eui.euiColorDarkShade} 25%, + transparent 25% + ), + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorDarkShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorDarkShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorDarkShade} 75% + ); + } + } + + &:hover, + &:focus, + .${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &:hover, + .${STATEFUL_EVENT_CSS_CLASS_NAME}:focus &:focus, + tr:hover &:hover, + tr:hover &:focus { + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; + + &, + & a, + & a:hover { + color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + } + + &::before { + background-image: linear-gradient( + 135deg, + ${({ theme }) => theme.eui.euiColorEmptyShade} 25%, + transparent 25% + ), + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorEmptyShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorEmptyShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorEmptyShade} 75% + ); + } + } + `} + + ${({ isDragging }) => + isDragging && + css` + & { + z-index: 9999 !important; + } + `} +`; + +ProviderContainerComponent.displayName = 'ProviderContainerComponent'; + +export const ProviderContainer = React.memo(ProviderContainerComponent); + +ProviderContainer.displayName = 'ProviderContainer'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx index 90d8ad463b476..ba0d53210bace 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx @@ -5,7 +5,7 @@ */ import { rgba } from 'polished'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; const Field = styled.div` diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx index f1ed533bef545..76335e3c72306 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; import { getEmptyString } from '../empty_value'; @@ -34,7 +33,7 @@ describe('draggables', () => { {'A child of this'} ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the default Badge', () => { @@ -50,7 +49,7 @@ describe('draggables', () => { {'A child of this'} ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 5b219dad9c841..57f047416ec1c 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -5,7 +5,7 @@ */ import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -105,12 +105,9 @@ export const DefaultDraggable = React.memo( ) : ( - + + {children} + ) } /> diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/duration/index.test.tsx index 140a625bc53fe..0dbc60ad9ae52 100644 --- a/x-pack/legacy/plugins/siem/public/components/duration/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/duration/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; import { ONE_MILLISECOND_AS_NANOSECONDS } from '../formatted_duration/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx index 15e6246f1f1ad..76712b789ffbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DefaultDraggable } from '../draggables'; import { FormattedDuration } from '../formatted_duration'; diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.test.tsx index 7c515862b0d92..1786905a4bb48 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx index 884d5bc348d6f..2dc3d8828675f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { Embeddable } from './embeddable'; @@ -18,6 +17,6 @@ describe('Embeddable', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx index aa247b69eb4eb..3b8e137618ab0 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx @@ -5,7 +5,6 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -15,7 +14,7 @@ describe('EmbeddableHeader', () => { test('it renders', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the title', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx index 007916595fd6a..c752273777d2f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { useIndexPatterns } from '../../hooks/use_index_patterns'; import { EmbeddedMapComponent } from './embedded_map'; @@ -41,6 +40,6 @@ describe('EmbeddedMapComponent', () => { startDate={new Date('2019-08-28T05:50:47.877Z').getTime()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx index d04329edff475..4f617644a1fe1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { IndexPatternsMissingPromptComponent } from './index_patterns_missing_prompt'; @@ -15,6 +14,6 @@ jest.mock('../../lib/kibana'); describe('IndexPatternsMissingPrompt', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx index 798e3d2c10f97..a4f95d2e299ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx @@ -6,7 +6,7 @@ import { EuiButton, EuiCode, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import * as React from 'react'; +import React from 'react'; import chrome from 'ui/chrome'; import { useKibana } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx index c43ab1ff4a036..824c717427763 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { LineToolTipContentComponent } from './line_tool_tip_content'; import { FeatureProperty } from '../types'; import { SUM_OF_DESTINATION_BYTES, SUM_OF_SOURCE_BYTES } from '../map_config'; @@ -27,6 +26,6 @@ describe('LineToolTipContent', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx index 13eefb252fb04..2daaeb53e45f2 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { MapToolTipComponent } from './map_tool_tip'; import { MapFeature } from '../types'; @@ -19,7 +18,7 @@ jest.mock('../../search_bar', () => ({ describe('MapToolTip', () => { test('placeholder component renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('full component renders correctly against snapshot', () => { @@ -46,6 +45,6 @@ describe('MapToolTip', () => { loadFeatureGeometry={loadFeatureGeometry} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 929b4983b5fd7..8741cfaa26ca6 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { FeatureProperty } from '../types'; import { getRenderedFieldValue, PointToolTipContentComponent } from './point_tool_tip_content'; import { TestProviders } from '../../../mock'; @@ -49,7 +48,7 @@ describe('PointToolTipContent', () => { /> ); - expect(toJson(wrapper.find('PointToolTipContentComponent'))).toMatchSnapshot(); + expect(wrapper.find('PointToolTipContentComponent')).toMatchSnapshot(); }); test('renders array filter correctly', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx index 4c77570cfbc9f..7351ea0a183c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ToolTipFooterComponent } from './tooltip_footer'; describe('ToolTipFilter', () => { @@ -27,7 +26,7 @@ describe('ToolTipFilter', () => { totalFeatures={100} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('Lower bounds', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx index 67b0c5ea64b51..6a14c12cee0f8 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { EmptyPage } from './index'; @@ -18,5 +17,5 @@ test('renders correctly', () => { title="My Super Title" /> ); - expect(toJson(EmptyComponent)).toMatchSnapshot(); + expect(EmptyComponent).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx b/x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx index bd056c04acc89..fc1d30907ab09 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx @@ -6,7 +6,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -25,7 +24,7 @@ describe('EmptyValue', () => { test('it renders against snapshot', () => { const wrapper = shallow(

{getEmptyString()}

); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('#getEmptyValue', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx index 6233fcfe7c823..6b90d9ccd08c4 100644 --- a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Provider } from 'react-redux'; import { apolloClientObservable, mockGlobalState } from '../../mock'; @@ -30,7 +29,7 @@ describe('Error Toast Dispatcher', () => {
); - expect(toJson(wrapper.find('Connect(ErrorToastDispatcherComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(ErrorToastDispatcherComponent)')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index d835d2c621931..1962850425baa 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiCheckbox, EuiFlexGroup, @@ -13,7 +15,7 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; @@ -23,16 +25,14 @@ import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard' import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; -import { DefaultDraggable } from '../draggables'; import { DraggableFieldBadge } from '../draggables/field_badge'; -import { EVENT_DURATION_FIELD_NAME } from '../duration'; import { FieldName } from '../fields_browser/field_name'; import { SelectableText } from '../selectable_text'; import { OverflowField } from '../tables/helpers'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; -import { DATE_FIELD_TYPE, MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; +import { MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; import { OnUpdateColumns } from '../timeline/events'; import { WithHoverActions } from '../with_hover_actions'; @@ -180,28 +180,14 @@ export const getColumns = ({ data.field === MESSAGE_FIELD_NAME ? ( ) : ( - - - - - + /> ) } /> diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index d97da7797bb45..162fc8fd8bb34 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; import { TestProviders } from '../../mock/test_providers'; @@ -34,7 +33,7 @@ describe('EventDetails', () => { toggleColumn={jest.fn()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx index 25f95bfa1d383..5b18e2d9b1fbc 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; import { TestProviders } from '../../mock/test_providers'; @@ -14,8 +14,6 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; import { useMountAppended } from '../../utils/use_mount_appended'; -jest.mock('../../lib/kibana'); - describe('EventFieldsBrowser', () => { const mount = useMountAppended(); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx index cec8ddff1a7c9..f4e9a2b789d71 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx @@ -6,7 +6,7 @@ import { sortBy } from 'lodash'; import { EuiInMemoryTable } from '@elastic/eui'; -import * as React from 'react'; +import React, { useMemo } from 'react'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; @@ -29,26 +29,35 @@ interface Props { /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = React.memo( ({ browserFields, columnHeaders, data, eventId, onUpdateColumns, timelineId, toggleColumn }) => { - const fieldsByName = getAllFieldsByName(browserFields); + const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); + const items = useMemo( + () => + sortBy(data, ['field']).map(item => ({ + ...item, + ...fieldsByName[item.field], + valuesConcatenated: item.values != null ? item.values.join() : '', + })), + [data, fieldsByName] + ); + const columns = useMemo( + () => + getColumns({ + browserFields, + columnHeaders, + eventId, + onUpdateColumns, + contextId: timelineId, + toggleColumn, + }), + [browserFields, columnHeaders, eventId, onUpdateColumns, timelineId, toggleColumn] + ); + return (
, column `render` callbacks expect complete BrowserField - items={sortBy(data, ['field']).map(item => { - return { - ...item, - ...fieldsByName[item.field], - valuesConcatenated: item.values != null ? item.values.join() : '', - }; - })} - columns={getColumns({ - browserFields, - columnHeaders, - eventId, - onUpdateColumns, - contextId: timelineId, - toggleColumn, - })} + items={items} + columns={columns} pagination={false} search={search} sorting={true} diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx index 429fc94b2f2d3..0cf158c8ea90b 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { mockDetailItemData } from '../../mock'; @@ -16,7 +15,7 @@ describe('JSON View', () => { describe('rendering', () => { test('should match snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx index 519f56adff2d2..9897e319e0487 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx @@ -6,7 +6,7 @@ import { EuiCodeEditor } from '@elastic/eui'; import { set } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DetailItem } from '../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx index b44d83c27a60d..3cef3e98c2f0a 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx @@ -18,8 +18,6 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; -jest.mock('../../lib/kibana'); - const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx index 27c3abf7f6824..1e225dabb2541 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx @@ -17,8 +17,6 @@ import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/f import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; -jest.mock('../../lib/kibana'); - const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx index 01317e754ad35..24118ace6796f 100644 --- a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx b/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx index bba32e72abc37..147d2e2d541f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx @@ -5,7 +5,7 @@ */ import { EuiIcon } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; const LinkIcon = styled(EuiIcon)` diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index e45f5dacb36a2..88d03d8db6761 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../graphql/types'; import { TestProviders } from '../../mock'; @@ -37,7 +36,7 @@ describe('Field Renderers', () => { locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete) ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders emptyTagValue when no fields provided', () => { @@ -61,7 +60,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow(dateRenderer(mockData.complete.source!.firstSeen)); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders emptyTagValue when invalid field provided', () => { @@ -79,7 +78,7 @@ describe('Field Renderers', () => { autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source) ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders emptyTagValue when non-string field provided', () => { @@ -111,7 +110,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders emptyTagValue when non-matching IP is provided', () => { @@ -154,7 +153,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders emptyTagValue when non-matching IP is provided', () => { @@ -188,7 +187,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow(whoisRenderer('10.10.10.10')); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); @@ -196,7 +195,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow({reputationRenderer('10.10.10.10')}); - expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); + expect(wrapper.find('DragDropContext')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx index 2d3accbcb55c8..361a0789135e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx index 3165580f435a2..d6972625821cf 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx @@ -5,7 +5,7 @@ */ import { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx index 4e16997ba92f6..38eaf43977fa2 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx index 7b8451db2212f..9d2a7da9b2d00 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx @@ -5,7 +5,7 @@ */ import { EuiInMemoryTable } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx index ce66a2d8d7919..cbead878f525d 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx index a4a53a5a51435..0c7dd7e908ce3 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiIcon, EuiFlexGroup, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx index e0628a410921e..792e0342a6d59 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx index 49255abc83dd5..cd14cef328a7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx index c43d5833fe1da..9214fd5f2540c 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx index c23908c396386..c8a0eb9da688b 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx @@ -102,138 +102,138 @@ type Props = Pick< * This component has no internal state, but it uses lifecycle methods to * set focus to the search input, scroll to the selected category, etc */ -export const FieldsBrowser = React.memo( - ({ - browserFields, - columnHeaders, - filteredBrowserFields, - isEventViewer, - isSearching, - onCategorySelected, - onFieldSelected, - onHideFieldBrowser, - onSearchInputChange, - onOutsideClick, - onUpdateColumns, - searchInput, - selectedCategoryId, - timelineId, - toggleColumn, - width, - }) => { - /** Focuses the input that filters the field browser */ - const focusInput = () => { - const elements = document.getElementsByClassName( - getFieldBrowserSearchInputClassName(timelineId) - ); +const FieldsBrowserComponent: React.FC = ({ + browserFields, + columnHeaders, + filteredBrowserFields, + isEventViewer, + isSearching, + onCategorySelected, + onFieldSelected, + onHideFieldBrowser, + onSearchInputChange, + onOutsideClick, + onUpdateColumns, + searchInput, + selectedCategoryId, + timelineId, + toggleColumn, + width, +}) => { + /** Focuses the input that filters the field browser */ + const focusInput = () => { + const elements = document.getElementsByClassName( + getFieldBrowserSearchInputClassName(timelineId) + ); - if (elements.length > 0) { - (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` + if (elements.length > 0) { + (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` + } + }; + + /** Invoked when the user types in the input to filter the field browser */ + const onInputChange = useCallback( + (event: React.ChangeEvent) => { + onSearchInputChange(event.target.value); + }, + [onSearchInputChange] + ); + + const selectFieldAndHide = useCallback( + (fieldId: string) => { + if (onFieldSelected != null) { + onFieldSelected(fieldId); } - }; - - /** Invoked when the user types in the input to filter the field browser */ - const onInputChange = useCallback( - (event: React.ChangeEvent) => { - onSearchInputChange(event.target.value); - }, - [onSearchInputChange] - ); - const selectFieldAndHide = useCallback( - (fieldId: string) => { - if (onFieldSelected != null) { - onFieldSelected(fieldId); - } + onHideFieldBrowser(); + }, + [onFieldSelected, onHideFieldBrowser] + ); + + const scrollViews = () => { + if (selectedCategoryId !== '') { + const categoryPaneTitles = document.getElementsByClassName( + getCategoryPaneCategoryClassName({ + categoryId: selectedCategoryId, + timelineId, + }) + ); - onHideFieldBrowser(); - }, - [onFieldSelected, onHideFieldBrowser] - ); + if (categoryPaneTitles.length > 0) { + categoryPaneTitles[0].scrollIntoView(); + } + + const fieldPaneTitles = document.getElementsByClassName( + getFieldBrowserCategoryTitleClassName({ + categoryId: selectedCategoryId, + timelineId, + }) + ); - const scrollViews = () => { - if (selectedCategoryId !== '') { - const categoryPaneTitles = document.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ); - - if (categoryPaneTitles.length > 0) { - categoryPaneTitles[0].scrollIntoView(); - } - - const fieldPaneTitles = document.getElementsByClassName( - getFieldBrowserCategoryTitleClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ); - - if (fieldPaneTitles.length > 0) { - fieldPaneTitles[0].scrollIntoView(); - } + if (fieldPaneTitles.length > 0) { + fieldPaneTitles[0].scrollIntoView(); } + } + + focusInput(); // always re-focus the input to enable additional filtering + }; + + useEffect(() => { + scrollViews(); + }, [selectedCategoryId, timelineId]); + + return ( + + +
+ + + + + + + + + + + + + ); +}; - focusInput(); // always re-focus the input to enable additional filtering - }; - - useEffect(() => { - scrollViews(); - }, [selectedCategoryId, timelineId]); - - return ( - - -
- - - - - - - - - - - - - ); - } -); +export const FieldsBrowser = React.memo(FieldsBrowserComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx index 6034f5a476443..4d0c707c46910 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx @@ -5,7 +5,7 @@ */ import { omit } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx index dc902b792d601..778e9d3d3c744 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiCheckbox, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { uniqBy } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx index 59ee2efa1f006..1437af7a30adb 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx index 68ba2e2774314..f3ec87a96d46b 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx index 170cf324ca6d8..fba6e22e4b21f 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx index 7e36a028961c4..42689065354d0 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx index 8acb19970c268..45b331f133e85 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx @@ -12,7 +12,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index 8a01a01b1daae..24e4cd77b20d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx index 3958cd463d56e..c8cde5fa02a51 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx @@ -212,9 +212,7 @@ export const StatefulFieldsBrowserComponent = React.memo { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx index edf6f7f01ab2e..b4d8c790002b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx @@ -13,7 +13,7 @@ import { gutterTimeline } from '../../lib/helpers'; const offsetChrome = 49; -const disableSticky = 'screen and (max-width: ' + euiLightVars.euiBreakpoints.s + ')'; +const disableSticky = `screen and (max-width: ${euiLightVars.euiBreakpoints.s})`; const disableStickyMq = window.matchMedia(disableSticky); const Wrapper = styled.aside<{ isSticky?: boolean }>` diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx index 47f0aef4b4806..f984b534c188d 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { FlowDirection } from '../../graphql/types'; @@ -25,7 +24,7 @@ describe('Select Flow Direction', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx index b1f757841e54b..373cc3cdc4a92 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx @@ -5,9 +5,8 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { clone } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { ActionCreator } from 'typescript-fsa'; import { FlowDirection, FlowTarget } from '../../graphql/types'; @@ -31,7 +30,7 @@ describe('FlowTargetSelect Component', () => { test('it renders the FlowTargetSelect', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx index fee4f25f9e255..6ec5912872467 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx @@ -6,7 +6,7 @@ import { EuiNotificationBadge, EuiIcon, EuiButton } from '@elastic/eui'; import { rgba } from 'polished'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index be7e8fac70bf5..83b842956e10e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -5,9 +5,8 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { set } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { ActionCreator } from 'typescript-fsa'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; @@ -35,7 +34,7 @@ describe('Flyout', () => { /> ); - expect(toJson(wrapper.find('Flyout'))).toMatchSnapshot(); + expect(wrapper.find('Flyout')).toMatchSnapshot(); }); test('it renders the default flyout state as a button', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 2d347830d5b1b..528f02f0a845b 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -6,7 +6,7 @@ import { EuiBadge } from '@elastic/eui'; import { defaultTo, getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 246261035508b..365f99c6667b8 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../mock'; import { flyoutHeaderHeight } from '..'; @@ -16,8 +15,6 @@ const testFlyoutHeight = 980; const testWidth = 640; const usersViewing = ['elastic']; -jest.mock('../../../lib/kibana'); - describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( @@ -34,7 +31,7 @@ describe('Pane', () => { ); - expect(toJson(EmptyComponent.find('Pane'))).toMatchSnapshot(); + 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', () => { @@ -53,7 +50,7 @@ describe('Pane', () => { ); - expect(wrapper.find('[data-test-subj="eui-flyout"]').get(0).props.maxWidth).toEqual('95%'); + expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); }); test('it applies timeline styles to the EuiFlyout', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index f2f0cf4f980f3..00ac15092a6ec 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -5,13 +5,14 @@ */ import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiToolTip } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; +import { Resizable, ResizeCallback } from 're-resizable'; +import { throttle } from 'lodash/fp'; -import { OnResize, Resizeable } from '../../resize_handle'; -import { TimelineResizeHandle } from '../../resize_handle/styled_handles'; +import { TimelineResizeHandle } from './timeline_resize_handle'; import { FlyoutHeader } from '../header'; import * as i18n from './translations'; @@ -41,10 +42,10 @@ interface DispatchProps { type Props = OwnProps & DispatchProps; -const EuiFlyoutContainer = styled.div<{ headerHeight: number; width: number }>` +const EuiFlyoutContainer = styled.div<{ headerHeight: number }>` .timeline-flyout { min-width: 150px; - width: ${({ width }) => `${width}px`}; + width: auto; } .timeline-flyout-header { align-items: center; @@ -65,8 +66,6 @@ const EuiFlyoutContainer = styled.div<{ headerHeight: number; width: number }>` } `; -EuiFlyoutContainer.displayName = 'EuiFlyoutContainer'; - const FlyoutHeaderContainer = styled.div` align-items: center; display: flex; @@ -75,88 +74,95 @@ const FlyoutHeaderContainer = styled.div` width: 100%; `; -FlyoutHeaderContainer.displayName = 'FlyoutHeaderContainer'; - // manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` const WrappedCloseButton = styled.div` margin-right: 5px; `; -WrappedCloseButton.displayName = 'WrappedCloseButton'; - -const FlyoutHeaderWithCloseButton = React.memo<{ +const FlyoutHeaderWithCloseButtonComponent: React.FC<{ onClose: () => void; timelineId: string; usersViewing: string[]; -}>( - ({ onClose, timelineId, usersViewing }) => ( - - - - - - - - - ), +}> = ({ onClose, timelineId, usersViewing }) => ( + + + + + + + + +); + +const FlyoutHeaderWithCloseButton = React.memo( + FlyoutHeaderWithCloseButtonComponent, (prevProps, nextProps) => prevProps.timelineId === nextProps.timelineId && prevProps.usersViewing === nextProps.usersViewing ); -FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; - -const FlyoutPaneComponent = React.memo( - ({ - applyDeltaToWidth, - children, - flyoutHeight, - headerHeight, - onClose, - timelineId, - usersViewing, - width, - }) => { - const renderFlyout = useCallback(() => <>, []); - - const onResize: OnResize = useCallback( - ({ delta, id }) => { - const bodyClientWidthPixels = document.body.clientWidth; - +const FlyoutPaneComponent: React.FC = ({ + applyDeltaToWidth, + children, + flyoutHeight, + headerHeight, + onClose, + timelineId, + usersViewing, + width, +}) => { + const [lastDelta, setLastDelta] = useState(0); + const onResizeStop: ResizeCallback = useCallback( + (e, direction, ref, delta) => { + const bodyClientWidthPixels = document.body.clientWidth; + + if (delta.width) { applyDeltaToWidth({ bodyClientWidthPixels, - delta, - id, + delta: -(delta.width - lastDelta), + id: timelineId, maxWidthPercent, minWidthPixels, }); - }, - [applyDeltaToWidth, maxWidthPercent, minWidthPixels] - ); - return ( - - - setLastDelta(0), [setLastDelta]); + const throttledResize = throttle(100, onResizeStop); + + return ( + + + - } - id={timelineId} - onResize={onResize} - render={renderFlyout} - /> + ), + }} + onResizeStart={resetLastDelta} + onResize={throttledResize} + > ( {children} - - - ); - } -); - -FlyoutPaneComponent.displayName = 'FlyoutPaneComponent'; + + + + ); +}; export const Pane = connect(null, { applyDeltaToWidth: timelineActions.applyDeltaToWidth, -})(FlyoutPaneComponent); +})(React.memo(FlyoutPaneComponent)); Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/styled_handles.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx similarity index 70% rename from x-pack/legacy/plugins/siem/public/components/resize_handle/styled_handles.tsx rename to x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx index 4f641c5d2042e..3ee29c2eaaa16 100644 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/styled_handles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx @@ -8,18 +8,13 @@ import styled from 'styled-components'; export const TIMELINE_RESIZE_HANDLE_WIDTH = 2; // px -export const CommonResizeHandle = styled.div` +export const TimelineResizeHandle = styled.div<{ height: number }>` cursor: col-resize; height: 100%; min-height: 20px; width: 0; -`; -CommonResizeHandle.displayName = 'CommonResizeHandle'; - -export const TimelineResizeHandle = styled(CommonResizeHandle)<{ height: number }>` border: ${TIMELINE_RESIZE_HANDLE_WIDTH}px solid ${props => props.theme.eui.euiColorLightShade}; z-index: 2; height: ${({ height }) => `${height}px`}; position: absolute; `; -TimelineResizeHandle.displayName = 'TimelineResizeHandle'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap index d23b1c61f7aee..ae30325f2a93b 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`formatted_bytes PreferenceFormattedBytes rendering renders correctly against snapshot 1`] = ` +exports[`PreferenceFormattedBytes renders correctly against snapshot 1`] = ` 2.7MB diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx index 8c27a55d3a6b0..914d233bccc1f 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx @@ -5,10 +5,8 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; -import { mockFrameworks, getMockKibanaUiSetting } from '../../mock'; import { useUiSetting$ } from '../../lib/kibana'; import { PreferenceFormattedBytesComponent } from '.'; @@ -16,50 +14,42 @@ import { PreferenceFormattedBytesComponent } from '.'; jest.mock('../../lib/kibana'); const mockUseUiSetting$ = useUiSetting$ as jest.Mock; -describe('formatted_bytes', () => { - describe('PreferenceFormattedBytes', () => { - describe('rendering', () => { - beforeEach(() => { - mockUseUiSetting$.mockClear(); - }); +const DEFAULT_BYTES_FORMAT_VALUE = '0,0.[0]b'; // kibana's default for this setting +const bytes = '2806422'; - const bytes = '2806422'; +describe('PreferenceFormattedBytes', () => { + test('renders correctly against snapshot', () => { + mockUseUiSetting$.mockImplementation(() => [DEFAULT_BYTES_FORMAT_VALUE]); + const wrapper = shallow(); - test('renders correctly against snapshot', () => { - mockUseUiSetting$.mockImplementation( - getMockKibanaUiSetting(mockFrameworks.default_browser) - ); - const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); - }); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders bytes to Numeral formatting when no format setting exists', () => { + mockUseUiSetting$.mockImplementation(() => [null]); + const wrapper = mount(); + + expect(wrapper.text()).toEqual('2,806,422'); + }); - test('it renders bytes to hardcoded format when no configuration exists', () => { - mockUseUiSetting$.mockImplementation(() => [null]); - const wrapper = mount(); - expect(wrapper.text()).toEqual('2.7MB'); - }); + test('it renders bytes according to the default format', () => { + mockUseUiSetting$.mockImplementation(() => [DEFAULT_BYTES_FORMAT_VALUE]); + const wrapper = mount(); - test('it renders bytes according to the default format', () => { - mockUseUiSetting$.mockImplementation( - getMockKibanaUiSetting(mockFrameworks.default_browser) - ); - const wrapper = mount(); - expect(wrapper.text()).toEqual('2.7MB'); - }); + expect(wrapper.text()).toEqual('2.7MB'); + }); + + test('it renders bytes supplied as a number according to the default format', () => { + mockUseUiSetting$.mockImplementation(() => [DEFAULT_BYTES_FORMAT_VALUE]); + const wrapper = mount(); + + expect(wrapper.text()).toEqual('2.7MB'); + }); - test('it renders bytes supplied as a number according to the default format', () => { - mockUseUiSetting$.mockImplementation( - getMockKibanaUiSetting(mockFrameworks.default_browser) - ); - const wrapper = mount(); - expect(wrapper.text()).toEqual('2.7MB'); - }); + test('it renders bytes according to new format', () => { + mockUseUiSetting$.mockImplementation(() => ['0b']); + const wrapper = mount(); - test('it renders bytes according to new format', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.bytes_short)); - const wrapper = mount(); - expect(wrapper.text()).toEqual('3MB'); - }); - }); + expect(wrapper.text()).toEqual('3MB'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx index 003ce0879b7b5..98a1acf471629 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx @@ -4,19 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import numeral from '@elastic/numeral'; import { DEFAULT_BYTES_FORMAT } from '../../../common/constants'; import { useUiSetting$ } from '../../lib/kibana'; -export const PreferenceFormattedBytesComponent = ({ value }: { value: string | number }) => { +type Bytes = string | number; + +export const formatBytes = (value: Bytes, format: string) => { + return numeral(value).format(format); +}; + +export const useFormatBytes = () => { const [bytesFormat] = useUiSetting$(DEFAULT_BYTES_FORMAT); - return ( - <>{bytesFormat ? numeral(value).format(bytesFormat) : numeral(value).format('0,0.[0]b')} - ); + + return (value: Bytes) => formatBytes(value, bytesFormat); }; +export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( + <>{useFormatBytes()(value)} +); + PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap index d196a23bff5bf..9e851ddcd7d0f 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`formatted_date PreferenceFormattedDate rendering renders correctly against snapshot 1`] = ` +exports[`formatted_date PreferenceFormattedDate renders correctly against snapshot 1`] = ` - 2019-02-25T22:27:05.000Z + 2019-02-25T22:27:05Z `; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx index dad1d5feb5c6e..0d8222ce85e1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx @@ -5,175 +5,165 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import moment from 'moment-timezone'; -import * as React from 'react'; +import React from 'react'; -import { useUiSetting$ } from '../../lib/kibana'; +import { useDateFormat, useTimeZone } from '../../hooks'; -import { mockFrameworks, TestProviders, MockFrameworks, getMockKibanaUiSetting } from '../../mock'; +import { TestProviders } from '../../mock'; import { getEmptyString, getEmptyValue } from '../empty_value'; import { PreferenceFormattedDate, FormattedDate, FormattedRelativePreferenceDate } from '.'; -jest.mock('../../lib/kibana'); -const mockUseUiSetting$ = useUiSetting$ as jest.Mock; +jest.mock('../../hooks'); +const mockUseDateFormat = useDateFormat as jest.Mock; +const mockUseTimeZone = useTimeZone as jest.Mock; + +const isoDateString = '2019-02-25T22:27:05.000Z'; describe('formatted_date', () => { + let isoDate: Date; + + beforeEach(() => { + isoDate = new Date(isoDateString); + mockUseDateFormat.mockImplementation(() => 'MMM D, YYYY @ HH:mm:ss.SSS'); + mockUseTimeZone.mockImplementation(() => 'UTC'); + }); + describe('PreferenceFormattedDate', () => { - describe('rendering', () => { - const isoDateString = '2019-02-25T22:27:05.000Z'; - const isoDate = new Date(isoDateString); - const configFormattedDateString = (dateString: string, config: MockFrameworks): string => - moment - .tz( - dateString, - config.dateFormatTz! === 'Browser' ? config.timezone! : config.dateFormatTz! - ) - .format(config.dateFormat); - - test('renders correctly against snapshot', () => { - mockUseUiSetting$.mockImplementation(() => [null]); - const wrapper = mount(); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - test('it renders the UTC ISO8601 date string supplied when no configuration exists', () => { - mockUseUiSetting$.mockImplementation(() => [null]); - const wrapper = mount(); - expect(wrapper.text()).toEqual(isoDateString); - }); - - test('it renders the UTC ISO8601 date supplied when the default configuration exists', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - - const wrapper = mount(); - expect(wrapper.text()).toEqual( - configFormattedDateString(isoDateString, mockFrameworks.default_UTC) - ); - }); - - test('it renders the correct tz when the default browser configuration exists', () => { - mockUseUiSetting$.mockImplementation( - getMockKibanaUiSetting(mockFrameworks.default_browser) - ); - const wrapper = mount(); - expect(wrapper.text()).toEqual( - configFormattedDateString(isoDateString, mockFrameworks.default_browser) - ); - }); - - test('it renders the correct tz when a non-UTC configuration exists', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_MT)); - const wrapper = mount(); - expect(wrapper.text()).toEqual( - configFormattedDateString(isoDateString, mockFrameworks.default_MT) - ); - }); + test('renders correctly against snapshot', () => { + mockUseDateFormat.mockImplementation(() => ''); + const wrapper = mount(); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the date with the default configuration', () => { + const wrapper = mount(); + + expect(wrapper.text()).toEqual('Feb 25, 2019 @ 22:27:05.000'); + }); + + test('it renders a UTC ISO8601 date string supplied when no date format configuration exists', () => { + mockUseDateFormat.mockImplementation(() => ''); + const wrapper = mount(); + + expect(wrapper.text()).toEqual('2019-02-25T22:27:05Z'); + }); + + test('it renders the correct timezone when a non-UTC configuration exists', () => { + mockUseTimeZone.mockImplementation(() => 'America/Denver'); + const wrapper = mount(); + + expect(wrapper.text()).toEqual('Feb 25, 2019 @ 15:27:05.000'); + }); + + test('it renders the date with a user-defined format', () => { + mockUseDateFormat.mockImplementation(() => 'MMM-DD-YYYY'); + const wrapper = mount(); + + expect(wrapper.text()).toEqual('Feb-25-2019'); }); }); describe('FormattedDate', () => { - describe('rendering', () => { - test('it renders against a numeric epoch', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount(); - expect(wrapper.text()).toEqual('May 28, 2019 @ 21:35:39.000'); - }); - - test('it renders against a string epoch', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount(); - expect(wrapper.text()).toEqual('May 28, 2019 @ 21:35:39.000'); - }); - - test('it renders against a ISO string', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('May 28, 2019 @ 22:04:49.957'); - }); - - test('it renders against an empty string as an empty string placeholder', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('(Empty String)'); - }); - - test('it renders against an null as a EMPTY_VALUE', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual(getEmptyValue()); - }); - - test('it renders against an undefined as a EMPTY_VALUE', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual(getEmptyValue()); - }); - - test('it renders against an invalid date time as just the string its self', () => { - mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.default_UTC)); - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Rebecca Evan Braden'); - }); + test('it renders against a numeric epoch', () => { + const wrapper = mount(); + expect(wrapper.text()).toEqual('May 28, 2019 @ 21:35:39.000'); + }); + + test('it renders against a string epoch', () => { + const wrapper = mount(); + expect(wrapper.text()).toEqual('May 28, 2019 @ 21:35:39.000'); + }); + + test('it renders against a ISO string', () => { + const wrapper = mount( + + ); + expect(wrapper.text()).toEqual('May 28, 2019 @ 22:04:49.957'); + }); + + test('it renders against an empty string as an empty string placeholder', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toEqual(getEmptyString()); + }); + + test('it renders against an null as a EMPTY_VALUE', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toEqual(getEmptyValue()); + }); + + test('it renders against an undefined as a EMPTY_VALUE', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toEqual(getEmptyValue()); + }); + + test('it renders against an invalid date time as just the string its self', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toEqual('Rebecca Evan Braden'); }); }); describe('FormattedRelativePreferenceDate', () => { - describe('rendering', () => { - test('renders time over an hour correctly against snapshot', () => { - const isoDateString = '2019-02-25T22:27:05.000Z'; - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="preference-time"]').exists()).toBe(true); - }); - test('renders time under an hour correctly against snapshot', () => { - const timeTwelveMinutesAgo = new Date(new Date().getTime() - 12 * 60 * 1000).toISOString(); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="relative-time"]').exists()).toBe(true); - }); - test('renders empty string value correctly', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toBe(getEmptyString()); - }); - - test('renders undefined value correctly', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('renders null value correctly', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); + test('renders time over an hour correctly against snapshot', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="preference-time"]').exists()).toBe(true); + }); + + test('renders time under an hour correctly against snapshot', () => { + const timeTwelveMinutesAgo = new Date(new Date().getTime() - 12 * 60 * 1000).toISOString(); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="relative-time"]').exists()).toBe(true); + }); + + test('renders empty string value correctly', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toBe(getEmptyString()); + }); + + test('renders undefined value correctly', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toBe(getEmptyValue()); + }); + + test('renders null value correctly', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toBe(getEmptyValue()); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index 19e8ec3f95d26..4e5903c02abf7 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -5,32 +5,19 @@ */ import moment from 'moment-timezone'; -import * as React from 'react'; +import React from 'react'; import { FormattedRelative } from '@kbn/i18n/react'; -import { useUiSetting$ } from '../../lib/kibana'; - -import { - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_TIMEZONE_BROWSER, -} from '../../../common/constants'; +import { useDateFormat, useTimeZone } from '../../hooks'; import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; import { getMaybeDate } from './maybe_date'; export const PreferenceFormattedDate = React.memo<{ value: Date }>(({ value }) => { - const [dateFormat] = useUiSetting$(DEFAULT_DATE_FORMAT); - const [dateFormatTz] = useUiSetting$(DEFAULT_DATE_FORMAT_TZ); - const [timezone] = useUiSetting$(DEFAULT_TIMEZONE_BROWSER); + const dateFormat = useDateFormat(); + const timeZone = useTimeZone(); - return ( - <> - {dateFormat && dateFormatTz && timezone - ? moment.tz(value, dateFormatTz === 'Browser' ? timezone : dateFormatTz).format(dateFormat) - : moment.utc(value).toISOString()} - - ); + return <>{moment.tz(value, timeZone).format(dateFormat)}; }); PreferenceFormattedDate.displayName = 'PreferenceFormattedDate'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx index 8afbafe57af4a..fa8d87cf4e82d 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { getFormattedDurationString } from './helpers'; import { FormattedDurationTooltip } from './tooltip'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx index 1372b3ef10920..6c11a7aad7ad2 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx @@ -5,7 +5,7 @@ */ import { EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx index 8dcb558122d01..48d34451404be 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx @@ -5,7 +5,7 @@ */ import { isArray, isEmpty, isString, uniq } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx index a45bed87829bf..098de39bbfef5 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/match_media'; @@ -23,6 +22,6 @@ describe('HeaderGlobal', () => { test('it renders', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index 633ff90524de6..83a70fd90d82b 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -6,7 +6,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -29,7 +28,7 @@ describe('HeaderPage', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the title', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx index fbd8642c01fac..2bc80be20e42d 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx @@ -6,7 +6,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -16,7 +15,7 @@ describe('HeaderSection', () => { test('it renders', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the title', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.test.tsx index 3a2e516fffb7e..26c5f499717e9 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.test.tsx @@ -6,7 +6,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 04d6d94d6624d..a2a0ffdde34a5 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -20,7 +20,7 @@ import * as i18n from './translations'; const InspectContainer = styled.div<{ showInspect: boolean }>` .euiButtonIcon { - ${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0')} + ${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0;')} transition: opacity ${props => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease; } `; diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/ip/index.test.tsx index 2dda3eca563fe..cac372a27180e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ip/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock/test_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -20,7 +19,7 @@ describe('Port', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the the ip address', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx index 8c327989963b4..49237c3bb1bb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx index 3842b7be67876..c4ea6ff63a0a7 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx index 950ab252ad0bd..955a57576dc8e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx index dcecc636d9f0f..69a795d0c8db7 100644 --- a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { getEmptyValue } from '../empty_value'; import { LastEventIndexKey } from '../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx index 87761a51a431f..59f2acba4121b 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx @@ -5,7 +5,6 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -19,7 +18,7 @@ describe('LinkIcon', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders an action button when onClick is provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx index 21580a0ac8664..dd5c3bf8bb5d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { encodeIpv6 } from '../../lib/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 9d8d01d2bb49a..f63d13fcda7f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -5,7 +5,7 @@ */ import { EuiLink } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { encodeIpv6 } from '../../lib/helpers'; import { getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx index 1c97e766345aa..48dd91922583f 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { Loader } from './index'; @@ -17,6 +16,6 @@ describe('rendering', () => { {'Loading'} ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx index cd437911ab589..8c39a3d6ffcbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel, EuiText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; const SpinnerFlexItem = styled(EuiFlexItem)` diff --git a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx index 5feb70edffb9a..c25f0d71833c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; import moment from 'moment-timezone'; -import * as React from 'react'; +import React from 'react'; import { LocalizedDateTooltip } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx b/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx index 82520ce2745c0..ed0b1b1689218 100644 --- a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import moment from 'moment'; -import * as React from 'react'; +import React from 'react'; export const LocalizedDateTooltip = React.memo<{ children: React.ReactNode; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx index 56c215218ad5e..de662c162fc0a 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Markdown } from '.'; @@ -98,7 +97,7 @@ describe('Markdown', () => { test('it renders the expected table content', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); @@ -152,7 +151,7 @@ describe('Markdown', () => { test('it renders the expected content containing a link', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx index 16e065ae721c9..30695c9d0c7e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import ReactMarkdown from 'react-markdown'; import styled from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx index 80ccd07c30249..59aae5abce5c4 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { MarkdownHintComponent } from './markdown_hint'; @@ -89,7 +88,7 @@ describe('MarkdownHintComponent ', () => { test('it renders the expected hints', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx index 5ecd1d4c9d2ad..199059670e4bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx @@ -5,7 +5,7 @@ */ import { EuiText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx index 87d4e072e4299..78a4c967ee0bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { shallow } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { MatrixHistogram } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts index 1eb5e96b86857..a7ef71f7a6a0d 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -6,7 +6,7 @@ import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts'; import { get, groupBy, map, toPairs } from 'lodash/fp'; -import numeral from '@elastic/numeral'; + import { UpdateDateRange, ChartSeriesData } from '../charts/common'; import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types'; @@ -87,7 +87,3 @@ export const getCustomChartData = ( }, formattedChartData); else return formattedChartData; }; - -export const bytesFormatter = (value: number) => { - return numeral(value).format('0,0.[0]b'); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx index 3966448f84df0..6ccc41546e558 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import { useAnomaliesTableData } from './use_anomalies_table_data'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts index bce99c943c7a5..48277b0b6fa52 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts @@ -15,11 +15,8 @@ import { errorToToaster } from '../api/error_to_toaster'; import * as i18n from './translations'; import { useUiSetting$ } from '../../../lib/kibana'; -import { - DEFAULT_ANOMALY_SCORE, - DEFAULT_TIMEZONE_BROWSER, - DEFAULT_KBN_VERSION, -} from '../../../../common/constants'; +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; +import { useTimeZone } from '../../../hooks'; interface Args { influencers?: InfluencerInput[]; @@ -67,9 +64,8 @@ export const useAnomaliesTableData = ({ const capabilities = useContext(MlCapabilitiesContext); const userPermissions = hasMlUserPermissions(capabilities); const [, dispatchToaster] = useStateToaster(); - const [timezone] = useUiSetting$(DEFAULT_TIMEZONE_BROWSER); + const timeZone = useTimeZone(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const siemJobIds = siemJobs.filter(job => job.isInstalled).map(job => job.id); @@ -95,11 +91,10 @@ export const useAnomaliesTableData = ({ earliestMs, latestMs, influencers: influencersInput, - dateFormatTz: timezone, + dateFormatTz: timeZone, maxRecords: 500, maxExamples: 10, }, - kbnVersion, abortCtrl.signal ); if (isSubscribed) { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts index e66d984a15294..10b2538d1e785 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts @@ -21,20 +21,15 @@ export interface Body { maxExamples: number; } -export const anomaliesTableData = async ( - body: Body, - kbnVersion: string, - signal: AbortSignal -): Promise => { +export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise => { const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, { method: 'POST', credentials: 'same-origin', body: JSON.stringify(body), headers: { - 'kbn-system-api': 'true', 'content-Type': 'application/json', - 'kbn-xsrf': kbnVersion, - 'kbn-version': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, signal, }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts index c1654a1648f2b..1333951028494 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts @@ -22,18 +22,14 @@ export interface Body { maxExamples: number; } -export const getMlCapabilities = async ( - kbnVersion: string, - signal: AbortSignal -): Promise => { +export const getMlCapabilities = async (signal: AbortSignal): Promise => { const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, { method: 'GET', credentials: 'same-origin', headers: { - 'kbn-system-api': 'true', 'content-Type': 'application/json', - 'kbn-xsrf': kbnVersion, - 'kbn-version': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, signal, }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx index 562e3c15675a7..c48a5590b49cf 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import toJson from 'enzyme-to-json'; import { shallow } from 'enzyme'; import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; @@ -22,7 +21,7 @@ describe('entity_draggable', () => { entityValue="entity-value" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('renders with entity name with entity value as text', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx index 615e83d208dd6..d49c3008b696c 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import toJson from 'enzyme-to-json'; import { mockAnomalies } from '../mock'; import { cloneDeep } from 'lodash/fp'; import { shallow, mount } from 'enzyme'; @@ -20,7 +19,7 @@ describe('create_influencers', () => { test('renders correctly against snapshot', () => { const wrapper = shallow({createInfluencers(anomalies.anomalies[0].influencers)}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns an empty string when influencers is undefined', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx index b8d6908df464e..cae05e26b115b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx @@ -11,8 +11,6 @@ import { getMlCapabilities } from '../api/get_ml_capabilities'; import { emptyMlCapabilities } from '../empty_ml_capabilities'; import { errorToToaster } from '../api/error_to_toaster'; import { useStateToaster } from '../../toasters'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; import * as i18n from './translations'; @@ -36,7 +34,6 @@ export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ c emptyMlCapabilitiesProvider ); const [, dispatchToaster] = useStateToaster(); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); useEffect(() => { let isSubscribed = true; @@ -44,7 +41,7 @@ export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ c async function fetchMlCapabilities() { try { - const mlCapabilities = await getMlCapabilities(kbnVersion, abortCtrl.signal); + const mlCapabilities = await getMlCapabilities(abortCtrl.signal); if (isSubscribed) { setCapabilities({ ...mlCapabilities, capabilitiesFetched: true }); } diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx index cf24d6c02a138..9ff0081f4359f 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { AnomalyScoreComponent } from './anomaly_score'; import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; @@ -36,7 +35,7 @@ describe('anomaly_scores', () => { narrowDateRange={narrowDateRange} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should not show a popover on initial render', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx index 759e84e36f4ac..3041134f669ee 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { AnomalyScoresComponent, createJobKey } from './anomaly_scores'; import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; @@ -36,7 +35,7 @@ describe('anomaly_scores', () => { narrowDateRange={narrowDateRange} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('renders spinner when isLoading is true is passed', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx index f00fb62d74ac3..7c8900bf77d95 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx @@ -5,8 +5,7 @@ */ import { shallow, mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { mockAnomalies } from '../mock'; import { createDescriptionList } from './create_description_list'; import { EuiDescriptionList } from '@elastic/eui'; @@ -35,7 +34,7 @@ describe('create_description_list', () => { )} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it calls the narrow date range function on click', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx index 0d389ae14a825..f7759bb74c3ab 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import toJson from 'enzyme-to-json'; import { mockAnomalies } from '../mock'; import { cloneDeep } from 'lodash/fp'; import { shallow } from 'enzyme'; @@ -22,13 +21,13 @@ describe('draggable_score', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('renders correctly against snapshot when the index is not included', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx index daac4835adb28..4e6484c23613f 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { Columns } from '../../paginated_table'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx index 2113d3b82f52e..0c823ef75cf26 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx index 8e7bedd8f872a..a04b8f4b99653 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx @@ -25,12 +25,10 @@ import { throwIfNotOk } from '../../hooks/api/api'; * Checks the ML Recognizer API to see if a given indexPattern has any compatible modules * * @param indexPatternName ES index pattern to check for compatible modules - * @param headers optional headers to add * @param signal to cancel request */ export const checkRecognizer = async ({ indexPatternName, - kbnVersion, signal, }: CheckRecognizerProps): Promise => { const response = await fetch( @@ -39,10 +37,9 @@ export const checkRecognizer = async ({ method: 'GET', credentials: 'same-origin', headers: { - 'kbn-system-api': 'true', 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, signal, } @@ -55,22 +52,16 @@ export const checkRecognizer = async ({ * Returns ML Module for given moduleId. Returns all modules if no moduleId specified * * @param moduleId id of the module to retrieve - * @param headers optional headers to add optional headers to add * @param signal to cancel request */ -export const getModules = async ({ - moduleId = '', - kbnVersion, - signal, -}: GetModulesProps): Promise => { +export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise => { const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/get_module/${moduleId}`, { method: 'GET', credentials: 'same-origin', headers: { - 'kbn-system-api': 'true', 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, signal, }); @@ -93,7 +84,6 @@ export const setupMlJob = async ({ indexPatternName = 'auditbeat-*', jobIdErrorFilter = [], groups = ['siem'], - kbnVersion, prefix = '', }: MlSetupArgs): Promise => { const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, { @@ -107,10 +97,9 @@ export const setupMlJob = async ({ useDedicatedIndex: true, }), headers: { - 'kbn-system-api': 'true', 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, }); await throwIfNotOk(response); @@ -124,16 +113,13 @@ export const setupMlJob = async ({ * * @param datafeedIds * @param start - * @param headers optional headers to add */ export const startDatafeeds = async ({ datafeedIds, - kbnVersion, start = 0, }: { datafeedIds: string[]; start: number; - kbnVersion: string; }): Promise => { const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, { method: 'POST', @@ -143,10 +129,9 @@ export const startDatafeeds = async ({ ...(start !== 0 && { start }), }), headers: { - 'kbn-system-api': 'true', 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, }); await throwIfNotOk(response); @@ -163,10 +148,8 @@ export const startDatafeeds = async ({ */ export const stopDatafeeds = async ({ datafeedIds, - kbnVersion, }: { datafeedIds: string[]; - kbnVersion: string; }): Promise<[StopDatafeedResponse | ErrorResponse, CloseJobsResponse]> => { const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, { method: 'POST', @@ -175,9 +158,9 @@ export const stopDatafeeds = async ({ datafeedIds, }), headers: { - 'kbn-system-api': 'true', 'content-type': 'application/json', - 'kbn-xsrf': kbnVersion, + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, }); @@ -198,7 +181,7 @@ export const stopDatafeeds = async ({ headers: { 'content-type': 'application/json', 'kbn-system-api': 'true', - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, }); @@ -214,10 +197,7 @@ export const stopDatafeeds = async ({ * * @param signal to cancel request */ -export const getJobsSummary = async ( - signal: AbortSignal, - kbnVersion: string -): Promise => { +export const getJobsSummary = async (signal: AbortSignal): Promise => { const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, { method: 'POST', credentials: 'same-origin', @@ -225,8 +205,7 @@ export const getJobsSummary = async ( headers: { 'content-type': 'application/json', 'kbn-system-api': 'true', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, signal, }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx index f9d110d711d07..9df93d087e166 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx @@ -13,7 +13,7 @@ import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_prov import { useStateToaster } from '../../toasters'; import { errorToToaster } from '../../ml/api/error_to_toaster'; import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_INDEX_KEY, DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import * as i18n from './translations'; import { createSiemJobs } from './use_siem_jobs_helpers'; @@ -34,7 +34,6 @@ export const useSiemJobs = (refetchData: boolean): Return => { const capabilities = useContext(MlCapabilitiesContext); const userPermissions = hasMlUserPermissions(capabilities); const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); useEffect(() => { @@ -47,11 +46,10 @@ export const useSiemJobs = (refetchData: boolean): Return => { try { // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ - getJobsSummary(abortCtrl.signal, kbnVersion), - getModules({ signal: abortCtrl.signal, kbnVersion }), + getJobsSummary(abortCtrl.signal), + getModules({ signal: abortCtrl.signal }), checkRecognizer({ indexPatternName: siemDefaultIndex, - kbnVersion, signal: abortCtrl.signal, }), ]); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx index 542529c628b72..a14ff789f1078 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { GroupsFilterPopoverComponent } from './groups_filter_popover'; import { mockSiemJobs } from '../../__mocks__/api'; import { SiemJob } from '../../types'; @@ -23,7 +22,7 @@ describe('GroupsFilterPopover', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('when a filter is clicked, it becomes checked ', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx index 0711cc1c87966..cbee724802156 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { JobsTableFiltersComponent } from './jobs_table_filters'; import { SiemJob } from '../../types'; import { cloneDeep } from 'lodash/fp'; @@ -23,7 +22,7 @@ describe('JobsTableFilters', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('when you click Elastic Jobs filter, state is updated and it is selected', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx index 91e5510f4938d..1186573e3e209 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx @@ -5,8 +5,7 @@ */ import { shallow, mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { isChecked, isFailure, isJobLoading, JobSwitchComponent } from './job_switch'; import { cloneDeep } from 'lodash/fp'; @@ -29,7 +28,7 @@ describe('JobSwitch', () => { onJobStateChange={onJobStateChangeMock} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should call onJobStateChange when the switch is clicked to be true/open', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx index 691d43a8b18b3..fa524d8ff3dbc 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -5,8 +5,7 @@ */ import { shallow, mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { JobsTableComponent } from './jobs_table'; import { mockSiemJobs } from '../__mocks__/api'; import { cloneDeep } from 'lodash/fp'; @@ -28,7 +27,7 @@ describe('JobsTableComponent', () => { onJobStateChange={onJobStateChangeMock} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should render the hyperlink which points specifically to the job id', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx index 73451d574d841..0d243a84aa192 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import chrome from 'ui/chrome'; import React, { useEffect, useState } from 'react'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx index 2e2445fe933bb..bf1802f42084e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx @@ -5,13 +5,12 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ShowingCountComponent } from './showing_count'; describe('ShowingCount', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx index 987c63be3f7be..bd7d696757ca6 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { MlPopover } from './ml_popover'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx index c34ed51d22994..307be06424ee3 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx @@ -10,8 +10,7 @@ import moment from 'moment'; import React, { useContext, useReducer, useState } from 'react'; import styled from 'styled-components'; -import { DEFAULT_KBN_VERSION } from '../../../common/constants'; -import { useKibana, useUiSetting$ } from '../../lib/kibana'; +import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, trackUiAction as track } from '../../lib/track_usage'; import { errorToToaster } from '../ml/api/error_to_toaster'; import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions'; @@ -97,7 +96,6 @@ export const MlPopover = React.memo(() => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [filterProperties, setFilterProperties] = useState(defaultFilterProps); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [isLoadingSiemJobs, siemJobs] = useSiemJobs(refreshToggle); const [, dispatchToaster] = useStateToaster(); const capabilities = useContext(MlCapabilitiesContext); @@ -114,7 +112,6 @@ export const MlPopover = React.memo(() => { indexPatternName: job.defaultIndexPattern, jobIdErrorFilter: [job.id], groups: job.groups, - kbnVersion, }); } catch (error) { errorToToaster({ title: i18n.CREATE_JOB_FAILURE, error, dispatchToaster }); @@ -132,14 +129,14 @@ export const MlPopover = React.memo(() => { if (enable) { const startTime = Math.max(latestTimestampMs, maxStartTime); try { - await startDatafeeds({ datafeedIds: [`datafeed-${job.id}`], kbnVersion, start: startTime }); + await startDatafeeds({ datafeedIds: [`datafeed-${job.id}`], start: startTime }); } catch (error) { track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE); errorToToaster({ title: i18n.START_JOB_FAILURE, error, dispatchToaster }); } } else { try { - await stopDatafeeds({ datafeedIds: [`datafeed-${job.id}`], kbnVersion }); + await stopDatafeeds({ datafeedIds: [`datafeed-${job.id}`] }); } catch (error) { track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE); errorToToaster({ title: i18n.STOP_JOB_FAILURE, error, dispatchToaster }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx index d409f5de200a4..e611a5234da5e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx @@ -5,13 +5,12 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { PopoverDescriptionComponent } from './popover_description'; describe('JobsTableFilters', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts index f8794c1963961..964ae8c8242d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts @@ -14,7 +14,6 @@ export interface Group { export interface CheckRecognizerProps { indexPatternName: string[]; - kbnVersion: string; signal: AbortSignal; } @@ -30,7 +29,6 @@ export interface RecognizerModule { export interface GetModulesProps { moduleId?: string; - kbnVersion: string; signal: AbortSignal; } @@ -97,7 +95,6 @@ export interface MlSetupArgs { jobIdErrorFilter: string[]; groups: string[]; prefix?: string; - kbnVersion: string; } /** diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx index c522b7750c414..2ba08073b25b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx @@ -5,13 +5,12 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { UpgradeContentsComponent } from './upgrade_contents'; describe('JobsTableFilters', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index d7061ba4efd9c..cae209a76fc1c 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { CONSTANTS } from '../url_state/constants'; import { SiemNavigationComponent } from './'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx index 840e2bf3f42dc..b9563b60f301b 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { navTabs } from '../../../pages/home/home_navigations'; import { SiemPageName } from '../../../pages/home/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx index 6b29068f3cd2d..f0dc67a5ff4c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { uniq } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { CertificateFingerprint, diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx index 22531983b2399..ecf162ebf2739 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { shallow } from 'enzyme'; import { asArrayIfExists } from '../../lib/helpers'; @@ -123,7 +122,7 @@ describe('Netflow', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(getNetflowInstance()); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders a destination label', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/index.tsx index 26e6986c386be..3d6c2a7b767cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { Fingerprints } from './fingerprints'; import { NetflowColumns } from './netflow_columns'; diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx index f006ec0f003c1..09fa5d9fe1596 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import { uniq } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DefaultDraggable } from '../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx index 0dc4a2271e207..f8a0256ff4d43 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { SourceDestination } from '../../source_destination'; diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx index adac7dede1f85..ab71dc301156f 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { uniq } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx index fc76780ef80c7..ca6abc90d317b 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { AddNote } from '.'; @@ -24,7 +23,7 @@ describe('AddNote', () => { updateNote={jest.fn()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the Cancel button when onCancelAddNote is provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx index 3ab556a4e5dc4..24db5c5ec8125 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import * as i18n from '../translations'; @@ -19,7 +18,7 @@ describe('NewNote', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders a tab labeled "Note"', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx index 64cbdf1c678c5..5a3439d53dd89 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx @@ -5,7 +5,7 @@ */ import { EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Markdown } from '../../markdown'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/columns.tsx b/x-pack/legacy/plugins/siem/public/components/notes/columns.tsx index 0c7a18d6076aa..32e10ac3eb77d 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/columns.tsx @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +/* eslint-disable react/display-name */ + +import React from 'react'; import { EuiTableDataType } from '@elastic/eui'; import { NoteCard } from './note_card'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx index 65e46f8d84c9f..c933055186e07 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx @@ -6,7 +6,7 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import moment from 'moment'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Note } from '../../lib/note'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.test.tsx index 2c16f85c78076..a927627353f69 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.tsx index 88e59ddd1419b..e02ebc2a25fd0 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.tsx @@ -5,7 +5,7 @@ */ import { EuiPanel } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { NoteCardBody } from './note_card_body'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx index b6cd23496b190..46e1bab37495a 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; @@ -24,7 +23,7 @@ describe('NoteCardBody', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the text of the note in an h1', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx index 94ea0a6ee3129..11761c8fd39b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx @@ -5,7 +5,7 @@ */ import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { WithCopyToClipboard } from '../../../lib/clipboard/with_copy_to_clipboard'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx index 0511ad264647a..3525a88fc2ddd 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx @@ -5,7 +5,7 @@ */ import moment from 'moment-timezone'; -import * as React from 'react'; +import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import * as i18n from '../translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.tsx index a227b5af38e85..e6aa0542df4b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.tsx @@ -5,7 +5,7 @@ */ import { EuiAvatar, EuiPanel } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import * as i18n from '../translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.test.tsx index 9c41d9d52a755..5d99375c38217 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.test.tsx @@ -5,7 +5,7 @@ */ import moment from 'moment-timezone'; -import * as React from 'react'; +import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { NoteCreated } from './note_created'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.tsx index c94f4021e1a4a..cdd0406c71450 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.tsx @@ -5,7 +5,7 @@ */ import { FormattedRelative } from '@kbn/i18n/react'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { LocalizedDateTooltip } from '../../localized_date_tooltip'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.test.tsx index dc52822eaff77..f70e841d1eefd 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx index 917ec3f1bf0b8..e061141bf43e7 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx @@ -5,7 +5,7 @@ */ import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { DeleteTimelineModal } from './delete_timeline_modal'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx index 1163fbba1572a..82fe0d1d162a4 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx @@ -6,7 +6,7 @@ import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import * as React from 'react'; +import React from 'react'; import * as i18n from '../translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx index 561eac000bbf7..a3c5371435e52 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx @@ -6,7 +6,7 @@ import { EuiButtonIconProps } from '@elastic/eui'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { DeleteTimelineModalButton } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 91480f20d8b00..41e13408c1e01 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -181,6 +181,7 @@ export interface QueryTimelineById { apolloClient: ApolloClient | ApolloClient<{}> | undefined; duplicate: boolean; timelineId: string; + onOpenTimeline?: (timeline: TimelineModel) => void; openTimeline?: boolean; updateIsLoading: ActionCreator<{ id: string; isLoading: boolean }>; updateTimeline: DispatchUpdateTimeline; @@ -190,6 +191,7 @@ export const queryTimelineById = ({ apolloClient, duplicate = false, timelineId, + onOpenTimeline, openTimeline = true, updateIsLoading, updateTimeline, @@ -209,7 +211,9 @@ export const queryTimelineById = ({ ); const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate); - if (updateTimeline) { + if (onOpenTimeline != null) { + onOpenTimeline(timeline); + } else if (updateTimeline) { updateTimeline({ duplicate, from: getOr(getDefaultFromValue(), 'dateRange.start', timeline), diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx index c9f52d9d204ae..520e2094fb336 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { wait } from '../../lib/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index c22c5fdbcfbc5..a97cfefaf0393 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -12,18 +12,20 @@ import { Dispatch } from 'redux'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all'; - import { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query'; import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; import { State, timelineSelectors } from '../../store'; +import { timelineDefaults, TimelineModel } from '../../store/timeline/model'; import { createTimeline as dispatchCreateNewTimeline, updateIsLoading as dispatchUpdateIsLoading, } from '../../store/timeline/actions'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { OpenTimeline } from './open_timeline'; import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; import { + ActionTimelineToShow, DeleteTimelines, EuiSearchBarQuery, OnDeleteSelected, @@ -41,14 +43,14 @@ import { OpenTimelineReduxProps, } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; -import { ColumnHeader } from '../timeline/body/column_headers/column_header'; -import { timelineDefaults } from '../../store/timeline/model'; interface OwnProps { apolloClient: ApolloClient; /** Displays open timeline in modal */ isModal: boolean; closeModalTimeline?: () => void; + hideActions?: ActionTimelineToShow[]; + onOpenTimeline?: (timeline: TimelineModel) => void; } export type OpenTimelineOwnProps = OwnProps & @@ -69,15 +71,17 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str /** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ export const StatefulOpenTimelineComponent = React.memo( ({ + apolloClient, + closeModalTimeline, + createNewTimeline, defaultPageSize, + hideActions = [], isModal = false, + onOpenTimeline, + timeline, title, - apolloClient, - closeModalTimeline, updateTimeline, updateIsLoading, - timeline, - createNewTimeline, }) => { /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< @@ -212,6 +216,7 @@ export const StatefulOpenTimelineComponent = React.memo( queryTimelineById({ apolloClient, duplicate, + onOpenTimeline, timelineId, updateIsLoading, updateTimeline, @@ -286,6 +291,7 @@ export const StatefulOpenTimelineComponent = React.memo( data-test-subj={'open-timeline-modal'} deleteTimelines={onDeleteOneTimeline} defaultPageSize={defaultPageSize} + hideActions={hideActions} isLoading={loading} itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} onAddTimelinesToFavorites={undefined} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx index 70bf7d2cbeb52..463111bd9735f 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx @@ -8,7 +8,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import moment from 'moment'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mockTimelineResults } from '../../../mock/timeline_results'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx index 2c35d5d8254cd..e0507c2370831 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx @@ -5,7 +5,7 @@ */ import { uniqBy } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { NotePreview } from './note_preview'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx index 8092172a29cef..7cefaf08d76cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx @@ -6,7 +6,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { getEmptyValue } from '../../empty_value'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx index 126a14f1f8e32..bb4a032734b5b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx @@ -6,7 +6,7 @@ import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { getEmptyValue, defaultToEmptyTag } from '../../empty_value'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx index dbc7199aac725..a1ca7812bba34 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; @@ -143,7 +143,7 @@ describe('OpenTimeline', () => { ).toBe(true); }); - test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => { + test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(true); + expect(props.actionTimelineToShow).toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => { + test('it does NOT show the delete action columns when is onDeleteSelected undefined and deleteTimelines is specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => { + test('it does NOT show the delete action columns when is onDeleteSelected provided and deleteTimelines is undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => { + test('it does NOT show the delete action when both onDeleteSelected and deleteTimelines are undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx index 3b4057c69a696..8aab02b495392 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -5,7 +5,7 @@ */ import { EuiPanel } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import { OpenTimelineProps } from './types'; @@ -57,6 +57,11 @@ export const OpenTimeline = React.memo( /> ( pageIndex={pageIndex} pageSize={pageSize} searchResults={searchResults} - showExtendedColumnsAndActions={onDeleteSelected != null && deleteTimelines != null} + showExtendedColumns={true} sortDirection={sortDirection} sortField={sortField} totalSearchResultsCount={totalSearchResultsCount} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx index e3dc6d974b5e0..ca8fa50c572fe 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -6,7 +6,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx index cd89eb8aad6f4..c530929a3c96e 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx @@ -7,39 +7,49 @@ import { EuiModal, EuiOverlayMask } from '@elastic/eui'; import React from 'react'; +import { TimelineModel } from '../../../store/timeline/model'; import { useApolloClient } from '../../../utils/apollo_context'; + import * as i18n from '../translations'; +import { ActionTimelineToShow } from '../types'; import { StatefulOpenTimeline } from '..'; export interface OpenTimelineModalProps { onClose: () => void; + hideActions?: ActionTimelineToShow[]; + modalTitle?: string; + onOpen?: (timeline: TimelineModel) => void; } const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px -export const OpenTimelineModal = React.memo(({ onClose }) => { - const apolloClient = useApolloClient(); - - if (!apolloClient) return null; - - return ( - - - - - - ); -}); +export const OpenTimelineModal = React.memo( + ({ hideActions = [], modalTitle, onClose, onOpen }) => { + const apolloClient = useApolloClient(); + + if (!apolloClient) return null; + + return ( + + + + + + ); + } +); OpenTimelineModal.displayName = 'OpenTimelineModal'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index a5abb42c2e3b6..2c3adb138b7ac 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; @@ -143,7 +143,7 @@ describe('OpenTimelineModal', () => { ).toBe(true); }); - test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => { + test('it shows the delete action when onDeleteSelected and deleteTimelines are specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(true); + expect(props.actionTimelineToShow).toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => { + test('it does NOT show the delete when is onDeleteSelected undefined and deleteTimelines is specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => { + test('it does NOT show the delete action when is onDeleteSelected provided and deleteTimelines is undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => { + test('it does NOT show extended columns when both onDeleteSelected and deleteTimelines are undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index b097d2e0d01eb..dcd0b37770583 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -5,10 +5,10 @@ */ import { EuiModalBody, EuiModalHeader } from '@elastic/eui'; -import * as React from 'react'; +import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; -import { OpenTimelineProps } from '../types'; +import { OpenTimelineProps, ActionTimelineToShow } from '../types'; import { SearchRow } from '../search_row'; import { TimelinesTable } from '../timelines_table'; import { TitleRow } from '../title_row'; @@ -19,10 +19,11 @@ export const HeaderContainer = styled.div` HeaderContainer.displayName = 'HeaderContainer'; -export const OpenTimelineModalBody = React.memo( +export const OpenTimelineModalBody = memo( ({ deleteTimelines, defaultPageSize, + hideActions = [], isLoading, itemIdToExpandedNotesRowMap, onAddTimelinesToFavorites, @@ -43,51 +44,61 @@ export const OpenTimelineModalBody = React.memo( sortField, title, totalSearchResultsCount, - }) => ( - <> - - - + }) => { + const actionsToShow = useMemo(() => { + const actions: ActionTimelineToShow[] = + onDeleteSelected != null && deleteTimelines != null + ? ['delete', 'duplicate'] + : ['duplicate']; + return actions.filter(action => !hideActions.includes(action)); + }, [onDeleteSelected, deleteTimelines, hideActions]); + return ( + <> + + + + + + + - + - - - - - - - - ) + + + ); + } ); OpenTimelineModalBody.displayName = 'OpenTimelineModalBody'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx index 9a70fd476e89e..66947a313f5e5 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx @@ -6,7 +6,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx index c25bba7b6b041..1a4708ed5af08 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx @@ -7,7 +7,7 @@ import { EuiFilterButtonProps } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { SearchRow } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx index fa16c8cfb7035..5765d31078bcf 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx @@ -14,7 +14,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import * as i18n from '../translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx index 749ba8672abea..eec11f571328f 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -8,7 +8,7 @@ import { EuiButtonIconProps } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; @@ -27,10 +27,11 @@ describe('#getActionsColumns', () => { mockResults = cloneDeep(mockTimelineResults); }); - test('it renders the delete timeline (trash icon) when showDeleteAction is true (because showExtendedColumnsAndActions is true)', () => { + test('it renders the delete timeline (trash icon) when actionTimelineToShow is including the action delete', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -53,10 +54,11 @@ describe('#getActionsColumns', () => { expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(true); }); - test('it does NOT render the delete timeline (trash icon) when showDeleteAction is false (because showExtendedColumnsAndActions is false)', () => { + test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow is NOT including the action delete', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -79,10 +81,65 @@ describe('#getActionsColumns', () => { expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(false); }); + test('it renders the duplicate icon timeline when actionTimelineToShow is including the action duplicate', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(true); + }); + + test('it does NOT render the duplicate timeline when actionTimelineToShow is NOT including the action duplicate)', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(false); + }); + test('it does NOT render the delete timeline (trash icon) when deleteTimelines is not provided', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -111,6 +168,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -141,6 +199,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -174,6 +233,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx index 3a8bc83fdc98a..2b8bd3339cca2 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx @@ -4,25 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { ACTION_COLUMN_WIDTH } from './common_styles'; import { DeleteTimelineModalButton } from '../delete_timeline_modal'; import * as i18n from '../translations'; -import { DeleteTimelines, OnOpenTimeline, OpenTimelineResult } from '../types'; +import { + ActionTimelineToShow, + DeleteTimelines, + OnOpenTimeline, + OpenTimelineResult, +} from '../types'; /** * Returns the action columns (e.g. delete, open duplicate timeline) */ export const getActionsColumns = ({ + actionTimelineToShow, onOpenTimeline, deleteTimelines, - showDeleteAction, }: { + actionTimelineToShow: ActionTimelineToShow[]; deleteTimelines?: DeleteTimelines; onOpenTimeline: OnOpenTimeline; - showDeleteAction: boolean; }) => { const openAsDuplicateColumn = { align: 'center', @@ -65,7 +72,10 @@ export const getActionsColumns = ({ width: ACTION_COLUMN_WIDTH, }; - return showDeleteAction && deleteTimelines != null - ? [openAsDuplicateColumn, deleteTimelineColumn] - : [openAsDuplicateColumn]; + return [ + actionTimelineToShow.includes('duplicate') ? openAsDuplicateColumn : null, + actionTimelineToShow.includes('delete') && deleteTimelines != null + ? deleteTimelineColumn + : null, + ].filter(action => action != null); }; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx index fa08df1df4785..0f2cda9d79f0b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx @@ -7,7 +7,7 @@ import { EuiButtonIconProps } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -37,6 +37,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -63,6 +64,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingNotes.length} @@ -89,6 +91,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={nullNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={nullNotes.length} @@ -115,6 +118,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptylNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptylNotes.length} @@ -143,6 +147,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -169,6 +174,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={nullSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={nullSavedObjectId.length} @@ -195,6 +201,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -231,6 +238,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -269,6 +277,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -311,6 +320,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -346,6 +356,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -377,6 +388,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -411,6 +423,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -442,6 +455,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingTitle} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingTitle.length} @@ -475,6 +489,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={withMissingSavedObjectIdAndTitle} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={withMissingSavedObjectIdAndTitle.length} @@ -508,6 +523,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={withJustWhitespaceTitle} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={withJustWhitespaceTitle.length} @@ -541,6 +557,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={withMissingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={withMissingSavedObjectId.length} @@ -571,6 +588,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -605,6 +623,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -637,6 +656,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -673,6 +693,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -704,6 +725,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -737,6 +759,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingDescription} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingDescription.length} @@ -771,6 +794,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={justWhitespaceDescription} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={justWhitespaceDescription.length} @@ -803,6 +827,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -834,6 +859,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -868,6 +894,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingUpdated} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingUpdated.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx index e4ab42bb37c55..0d3a73a389050 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiButtonIcon, EuiLink } from '@elastic/eui'; import { omit } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { ACTION_COLUMN_WIDTH } from './common_styles'; import { isUntitled } from '../helpers'; @@ -25,11 +27,9 @@ export const getCommonColumns = ({ itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, - showExtendedColumnsAndActions, }: { onOpenTimeline: OnOpenTimeline; onToggleShowNotes: OnToggleShowNotes; - showExtendedColumnsAndActions: boolean; itemIdToExpandedNotesRowMap: Record; }) => [ { diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx index 13362e0f43a28..4cbe1e45c473b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; @@ -35,6 +35,7 @@ describe('#getExtendedColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -66,6 +67,7 @@ describe('#getExtendedColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -99,6 +101,7 @@ describe('#getExtendedColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingUpdatedBy} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingUpdatedBy.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx index 63f6b8b674a3d..b6d874fa0c4d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +/* eslint-disable react/display-name */ + +import React from 'react'; import { defaultToEmptyTag } from '../../empty_value'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx index b6048b85eea75..31377d176acac 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; @@ -30,6 +30,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -57,6 +58,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={with6Events} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={with6Events.length} @@ -82,6 +84,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -109,6 +112,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={with4Notes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={with4Notes.length} @@ -134,6 +138,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -161,6 +166,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={undefinedFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={undefinedFavorite.length} @@ -187,6 +193,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={nullFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={nullFavorite.length} @@ -213,6 +220,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptyFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptyFavorite.length} @@ -249,6 +257,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptyFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptyFavorite.length} @@ -289,6 +298,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptyFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptyFavorite.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx index 6d8a792e91b02..5b0f3ded7d71b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiIcon, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { ACTION_COLUMN_WIDTH } from './common_styles'; import { getNotesCount, getPinnedEventCount } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx index d75863d1ccb8b..26d9607a91fcd 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; @@ -28,10 +28,11 @@ describe('TimelinesTable', () => { mockResults = cloneDeep(mockTimelineResults); }); - test('it renders the select all timelines header checkbox when showExtendedColumnsAndActions is true', () => { + test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -59,10 +60,11 @@ describe('TimelinesTable', () => { ).toBe(true); }); - test('it does NOT render the select all timelines header checkbox when showExtendedColumnsAndActions is false', () => { + test('it does NOT render the select all timelines header checkbox when actionTimelineToShow has not the action selectable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -90,10 +92,11 @@ describe('TimelinesTable', () => { ).toBe(false); }); - test('it renders the Modified By column when showExtendedColumnsAndActions is true ', () => { + test('it renders the Modified By column when showExtendedColumns is true ', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -121,10 +124,11 @@ describe('TimelinesTable', () => { ).toContain(i18n.MODIFIED_BY); }); - test('it renders the notes column in the position of the Modified By column when showExtendedColumnsAndActions is false', () => { + test('it renders the notes column in the position of the Modified By column when showExtendedColumns is false', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -148,16 +152,17 @@ describe('TimelinesTable', () => { wrapper .find('thead tr th') .at(5) - .find('[data-test-subj="notes-count-header-icon"]') + .find('svg[data-test-subj="notes-count-header-icon"]') .first() .exists() ).toBe(true); }); - test('it renders the delete timeline (trash icon) when showExtendedColumnsAndActions is true', () => { + test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -185,10 +190,11 @@ describe('TimelinesTable', () => { ).toBe(true); }); - test('it does NOT render the delete timeline (trash icon) when showExtendedColumnsAndActions is false', () => { + test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow has NOT the delete action', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -216,10 +222,11 @@ describe('TimelinesTable', () => { ).toBe(false); }); - test('it renders the rows per page selector when showExtendedColumnsAndActions is true', () => { + test('it renders the rows per page selector when showExtendedColumns is true', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -247,10 +254,11 @@ describe('TimelinesTable', () => { ).toBe(true); }); - test('it does NOT render the rows per page selector when showExtendedColumnsAndActions is false', () => { + test('it does NOT render the rows per page selector when showExtendedColumns is false', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -284,6 +292,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={defaultPageSize} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -311,10 +320,11 @@ describe('TimelinesTable', () => { ).toEqual('Rows per page: 123'); }); - test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is true ', () => { + test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -342,10 +352,11 @@ describe('TimelinesTable', () => { ).toContain(i18n.LAST_MODIFIED); }); - test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is false ', () => { + test('it sorts the Last Modified column in descending order when showExtendedColumns is false ', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -376,6 +387,7 @@ describe('TimelinesTable', () => { test('it displays the expected message when no search results are found', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={[]} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={0} @@ -408,6 +420,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -446,6 +459,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -479,6 +493,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -510,6 +525,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx index 8f25b5345988a..f09a9f6af048b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx @@ -5,11 +5,12 @@ */ import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import * as i18n from '../translations'; import { + ActionTimelineToShow, DeleteTimelines, OnOpenTimeline, OnSelectionChange, @@ -36,8 +37,8 @@ const BasicTable = styled(EuiBasicTable)` `; BasicTable.displayName = 'BasicTable'; -const getExtendedColumnsIfEnabled = (showExtendedColumnsAndActions: boolean) => - showExtendedColumnsAndActions ? [...getExtendedColumns()] : []; +const getExtendedColumnsIfEnabled = (showExtendedColumns: boolean) => + showExtendedColumns ? [...getExtendedColumns()] : []; /** * Returns the column definitions (passed as the `columns` prop to @@ -46,34 +47,36 @@ const getExtendedColumnsIfEnabled = (showExtendedColumnsAndActions: boolean) => * `Timelines` page */ const getTimelinesTableColumns = ({ + actionTimelineToShow, deleteTimelines, itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, - showExtendedColumnsAndActions, + showExtendedColumns, }: { + actionTimelineToShow: ActionTimelineToShow[]; deleteTimelines?: DeleteTimelines; itemIdToExpandedNotesRowMap: Record; onOpenTimeline: OnOpenTimeline; onToggleShowNotes: OnToggleShowNotes; - showExtendedColumnsAndActions: boolean; + showExtendedColumns: boolean; }) => [ ...getCommonColumns({ itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, - showExtendedColumnsAndActions, }), - ...getExtendedColumnsIfEnabled(showExtendedColumnsAndActions), + ...getExtendedColumnsIfEnabled(showExtendedColumns), ...getIconHeaderColumns(), ...getActionsColumns({ deleteTimelines, onOpenTimeline, - showDeleteAction: showExtendedColumnsAndActions, + actionTimelineToShow, }), ]; export interface TimelinesTableProps { + actionTimelineToShow: ActionTimelineToShow[]; deleteTimelines?: DeleteTimelines; defaultPageSize: number; loading: boolean; @@ -85,7 +88,7 @@ export interface TimelinesTableProps { pageIndex: number; pageSize: number; searchResults: OpenTimelineResult[]; - showExtendedColumnsAndActions: boolean; + showExtendedColumns: boolean; sortDirection: 'asc' | 'desc'; sortField: string; totalSearchResultsCount: number; @@ -97,6 +100,7 @@ export interface TimelinesTableProps { */ export const TimelinesTable = React.memo( ({ + actionTimelineToShow, deleteTimelines, defaultPageSize, loading: isLoading, @@ -108,13 +112,13 @@ export const TimelinesTable = React.memo( pageIndex, pageSize, searchResults, - showExtendedColumnsAndActions, + showExtendedColumns, sortField, sortDirection, totalSearchResultsCount, }) => { const pagination = { - hidePerPageOptions: !showExtendedColumnsAndActions, + hidePerPageOptions: !showExtendedColumns, pageIndex, pageSize, pageSizeOptions: [ @@ -142,16 +146,17 @@ export const TimelinesTable = React.memo( return ( ( noItemsMessage={i18n.ZERO_TIMELINES_MATCH} onChange={onTableChange} pagination={pagination} - selection={showExtendedColumnsAndActions ? selection : undefined} + selection={actionTimelineToShow.includes('selectable') ? selection : undefined} sorting={sorting} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx index 9303c09c994aa..88dfab470ac96 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx @@ -7,7 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { EuiButtonProps } from '@elastic/eui'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import * as React from 'react'; +import React from 'react'; import { ThemeProvider } from 'styled-components'; import { TitleRow } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx index 2e82c4979f41d..c7de367e04364 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import * as i18n from '../translations'; import { OpenTimelineProps } from '../types'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts index 7bbefb9efa99e..e5e85ccf0954a 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts @@ -95,6 +95,8 @@ export interface OnTableChangeParams { /** Invoked by the EUI table implementation when the user interacts with the table */ export type OnTableChange = (tableChange: OnTableChangeParams) => void; +export type ActionTimelineToShow = 'duplicate' | 'delete' | 'selectable'; + export interface OpenTimelineProps { /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ deleteTimelines?: DeleteTimelines; @@ -140,6 +142,8 @@ export interface OpenTimelineProps { title: string; /** The total (server-side) count of the search results */ totalSearchResultsCount: number; + /** Hide action on timeline if needed it */ + hideActions?: ActionTimelineToShow[]; } export interface UpdateTimeline { diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx index 70e75cb54671a..345701c97901f 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../mock'; import { createStore, State } from '../../../store'; @@ -63,7 +62,7 @@ describe('AddFilterToGlobalSearchBar Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('Rendering tooltip', async () => { diff --git a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d2579f427debe..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HistogramSignals it renders 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.test.tsx deleted file mode 100644 index ad1d80a761854..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import React from 'react'; - -import { TestProviders } from '../../../../mock'; -import { HistogramSignals } from './index'; - -describe('HistogramSignals', () => { - test('it renders', () => { - const wrapper = shallow( - - - - ); - - expect(toJson(wrapper.find('HistogramSignals'))).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.tsx deleted file mode 100644 index 35fe8a2d90509..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.tsx +++ /dev/null @@ -1,79 +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 { - Axis, - Chart, - HistogramBarSeries, - Settings, - niceTimeFormatByDay, - timeFormatter, -} from '@elastic/charts'; -import React from 'react'; -import { npStart } from 'ui/new_platform'; - -export const HistogramSignals = React.memo(() => { - const sampleChartData = [ - { x: 1571090784000, y: 2, a: 'a' }, - { x: 1571090784000, y: 2, b: 'b' }, - { x: 1571093484000, y: 7, a: 'a' }, - { x: 1571096184000, y: 3, a: 'a' }, - { x: 1571098884000, y: 2, a: 'a' }, - { x: 1571101584000, y: 7, a: 'a' }, - { x: 1571104284000, y: 3, a: 'a' }, - { x: 1571106984000, y: 2, a: 'a' }, - { x: 1571109684000, y: 7, a: 'a' }, - { x: 1571112384000, y: 3, a: 'a' }, - { x: 1571115084000, y: 2, a: 'a' }, - { x: 1571117784000, y: 7, a: 'a' }, - { x: 1571120484000, y: 3, a: 'a' }, - { x: 1571123184000, y: 2, a: 'a' }, - { x: 1571125884000, y: 7, a: 'a' }, - { x: 1571128584000, y: 3, a: 'a' }, - { x: 1571131284000, y: 2, a: 'a' }, - { x: 1571133984000, y: 7, a: 'a' }, - { x: 1571136684000, y: 3, a: 'a' }, - { x: 1571139384000, y: 2, a: 'a' }, - { x: 1571142084000, y: 7, a: 'a' }, - { x: 1571144784000, y: 3, a: 'a' }, - { x: 1571147484000, y: 2, a: 'a' }, - { x: 1571150184000, y: 7, a: 'a' }, - { x: 1571152884000, y: 3, a: 'a' }, - { x: 1571155584000, y: 2, a: 'a' }, - { x: 1571158284000, y: 7, a: 'a' }, - { x: 1571160984000, y: 3, a: 'a' }, - { x: 1571163684000, y: 2, a: 'a' }, - { x: 1571166384000, y: 7, a: 'a' }, - { x: 1571169084000, y: 3, a: 'a' }, - { x: 1571171784000, y: 2, a: 'a' }, - { x: 1571174484000, y: 7, a: 'a' }, - ]; - - return ( - - - - - - - - - - ); -}); -HistogramSignals.displayName = 'HistogramSignals'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx index 71e61e2425373..d7c25e97b3838 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState } from '../../../../mock'; @@ -49,7 +48,7 @@ describe('Authentication Table Component', () => { ); - expect(toJson(wrapper.find('Connect(AuthenticationTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(AuthenticationTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx index ad5755068e662..f5485922647ca 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { has } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect } from 'react-redux'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx index 35c1eded18f15..4a836333f3311 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx @@ -5,7 +5,7 @@ */ import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { render, act } from '@testing-library/react'; @@ -15,8 +15,6 @@ import { TestProviders } from '../../../../mock'; import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; -jest.mock('../../../../lib/kibana'); - describe('FirstLastSeen Component', () => { const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx index 830665f827301..90cfe696610d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; import { HostOverview } from './index'; @@ -31,7 +30,7 @@ describe('Host Summary Component', () => { ); - expect(toJson(wrapper.find('HostOverview'))).toMatchSnapshot(); + expect(wrapper.find('HostOverview')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index 4728925eb741a..e561594013dea 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { @@ -61,7 +60,7 @@ describe('Hosts Table', () => { ); - expect(toJson(wrapper.find('HostsTable'))).toMatchSnapshot(); + expect(wrapper.find('HostsTable')).toMatchSnapshot(); }); describe('Sorting on Table', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx index 577ec5ff51470..dc2340d42ebd9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx @@ -7,7 +7,6 @@ import { mockKpiHostsData, mockKpiHostDetailsData } from './mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { KpiHostsComponentBase } from '.'; import * as statItems from '../../../stat_items'; import { kpiHostsMapping } from './kpi_hosts_mapping'; @@ -30,7 +29,7 @@ describe('kpiHostsComponent', () => { narrowDateRange={narrowDateRange} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it should render KpiHostsData', () => { @@ -44,7 +43,7 @@ describe('kpiHostsComponent', () => { narrowDateRange={narrowDateRange} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it should render KpiHostDetailsData', () => { @@ -58,7 +57,7 @@ describe('kpiHostsComponent', () => { narrowDateRange={narrowDateRange} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx index 28ddb1df12c3a..76fc2a0c389c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; import { hostsModel } from '../../../../store'; @@ -45,7 +44,7 @@ describe('Uncommon Process Table Component', () => { ); - expect(toJson(wrapper.find('UncommonProcessTable'))).toMatchSnapshot(); + expect(wrapper.find('UncommonProcessTable')).toMatchSnapshot(); }); test('it has a double dash (empty value) without any hosts at all', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx index a8b9b2a1f61ce..b4c01053f0e9c 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/index.tsx index 490efd08f0aa7..e7b0d8e7d00d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/index.tsx @@ -9,22 +9,23 @@ import React from 'react'; import { ScaleType } from '@elastic/charts'; import * as i18n from './translation'; import { MatrixHistogram } from '../../../matrix_histogram'; -import { bytesFormatter } from '../../../matrix_histogram/utils'; import { MatrixOverOrdinalHistogramData } from '../../../../graphql/types'; import { MatrixHistogramBasicProps } from '../../../matrix_histogram/types'; +import { useFormatBytes } from '../../../formatted_bytes'; export const NetworkDnsHistogram = ( props: MatrixHistogramBasicProps ) => { const dataKey = 'histogram'; const { ...matrixOverTimeProps } = props; + const formatBytes = useFormatBytes(); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx index c9d18d5f996f3..8c744c6573ee8 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; import { createStore, State } from '../../../../store'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx index 6c2edd764855c..3038d7f41c632 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ActionCreator } from 'typescript-fsa'; import { FlowTarget } from '../../../../graphql/types'; @@ -52,7 +51,7 @@ describe('IP Overview Component', () => { ); - expect(toJson(wrapper.find('IpOverview'))).toMatchSnapshot(); + expect(wrapper.find('IpOverview')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx index 964617c4c85b1..48d3b25f59e4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState } from '../../../../mock'; @@ -42,7 +41,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); + expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); }); test('it renders the default widget', () => { @@ -59,7 +58,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); + expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx index d59d4ccd60c60..73602fe3400a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx @@ -146,6 +146,8 @@ export const KpiNetworkBaseComponent = React.memo<{ ); }); +KpiNetworkBaseComponent.displayName = 'KpiNetworkBaseComponent'; + export const KpiNetworkComponent = React.memo( ({ data, from, id, loading, to, narrowDateRange }) => { return loading ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx index b88653bcadde8..e425057dd0f75 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -50,7 +49,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper.find('Connect(NetworkDnsTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(NetworkDnsTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx index e39723f57f0b0..31a1b1667087a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { FlowDirection } from '../../../../graphql/types'; @@ -19,7 +18,7 @@ describe('NetworkTopNFlow Select direction', () => { test('it renders the basic switch to include PTR in table', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx index 6a47a58c85f31..bffc7235b6804 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import numeral from '@elastic/numeral'; import { NetworkHttpEdges, NetworkHttpFields, NetworkHttpItem } from '../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx index 81e0c7fad7b39..6ce87728ebff7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -51,7 +50,7 @@ describe('NetworkHttp Table Component', () => { ); - expect(toJson(wrapper.find('Connect(NetworkHttpTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(NetworkHttpTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx index 8fd245b077243..764e440a5a4be 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -59,7 +58,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); }); test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( @@ -84,7 +83,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx index 5c4aa862283f2..24f68ef03d891 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -59,7 +58,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(NetworkTopNFlowTableComponent)')).toMatchSnapshot(); }); test('it renders the default NetworkTopNFlow table on the IP Details page', () => { @@ -85,7 +84,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(NetworkTopNFlowTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx index aea8ee9e6b9e1..44a538871d951 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import moment from 'moment'; import { TlsNode } from '../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx index 920d1cd8210e5..81a472f3175e5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -47,7 +46,7 @@ describe('Tls Table Component', () => { ); - expect(toJson(wrapper.find('Connect(TlsTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(TlsTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx index d01923f01543f..b23c7bd504fb7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -55,7 +54,7 @@ describe('Users Table Component', () => { ); - expect(toJson(wrapper.find('Connect(UsersTableComponent)'))).toMatchSnapshot(); + expect(wrapper.find('Connect(UsersTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx index d324df2ecb0c1..f99b2687d7072 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { OverviewHostStats } from '.'; import { mockData } from './mock'; @@ -15,7 +14,7 @@ describe('Overview Host Stat Data', () => { describe('rendering', () => { test('it renders the default OverviewHostStats', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); describe('loading', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx index 1b28c032221ab..08093c5d38c15 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { OverviewNetworkStats } from '.'; import { mockData } from './mock'; @@ -17,7 +16,7 @@ describe('Overview Network Stat Data', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); describe('loading', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx index 0444360d2b965..947bdee6a5cd2 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Direction } from '../../graphql/types'; @@ -58,7 +57,7 @@ describe('Paginated Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the loading panel at the beginning ', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/pin/index.tsx b/x-pack/legacy/plugins/siem/public/components/pin/index.tsx index 8aec3a60cbc37..9f898f9acaf2e 100644 --- a/x-pack/legacy/plugins/siem/public/components/pin/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/pin/index.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, IconSize } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import * as i18n from '../../components/timeline/body/translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx index 330385e39ca79..6ab587f266a8a 100644 --- a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../mock/test_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -20,7 +19,7 @@ describe('Port', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the port', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.tsx b/x-pack/legacy/plugins/siem/public/components/port/index.tsx index a8961a92f24cf..bd6289547d0dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/port/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/port/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DefaultDraggable } from '../draggables'; import { getEmptyValue } from '../empty_value'; diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx index 8ecc50402cef1..e5c39b365d979 100644 --- a/x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { ProgressInline } from './index'; @@ -18,6 +17,6 @@ describe('ProgressInline', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx index e403963cbbe20..870d0b40d8cd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -14,8 +14,6 @@ import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/ import { QueryBar, QueryBarComponentProps } from '.'; import { createKibanaContextProviderMock } from '../../mock/kibana_react'; -jest.mock('../../lib/kibana'); - const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; describe('QueryBar ', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 38027f80e6684..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Resizeable it renders 1`] = ` - - - - - -`; diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx deleted file mode 100644 index f1580b59362bf..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; - -import { TestProviders } from '../../mock/test_providers'; - -import { - addGlobalResizeCursorStyleToBody, - globalResizeCursorClassName, - removeGlobalResizeCursorStyleFromBody, - Resizeable, - calculateDeltaX, -} from '.'; -import { CommonResizeHandle } from './styled_handles'; - -describe('Resizeable', () => { - afterEach(() => { - document.body.classList.remove(globalResizeCursorClassName); - }); - - test('it applies the provided height to the ResizeHandleContainer when a height is specified', () => { - const wrapper = mount( - - } - height="100%" - id="test" - onResize={jest.fn()} - render={() => <>} - /> - - ); - - expect(wrapper.find('[data-test-subj="resize-handle-container"]').first()).toHaveStyleRule( - 'height', - '100%' - ); - }); - - test('it applies positioning styles to the ResizeHandleContainer when positionAbsolute is true and bottom/left/right/top is specified', () => { - const wrapper = mount( - - } - id="test" - left={0} - onResize={jest.fn()} - positionAbsolute - render={() => <>} - right={0} - top={0} - /> - - ); - const resizeHandleContainer = wrapper - .find('[data-test-subj="resize-handle-container"]') - .first(); - - expect(resizeHandleContainer).toHaveStyleRule('bottom', '0'); - expect(resizeHandleContainer).toHaveStyleRule('left', '0'); - expect(resizeHandleContainer).toHaveStyleRule('position', 'absolute'); - expect(resizeHandleContainer).toHaveStyleRule('right', '0'); - expect(resizeHandleContainer).toHaveStyleRule('top', '0'); - }); - - test('it DOES NOT apply positioning styles to the ResizeHandleContainer when positionAbsolute is false, regardless if bottom/left/right/top is specified', () => { - const wrapper = mount( - - } - id="test" - left={0} - onResize={jest.fn()} - render={() => <>} - right={0} - top={0} - /> - - ); - const resizeHandleContainer = wrapper - .find('[data-test-subj="resize-handle-container"]') - .first(); - - expect(resizeHandleContainer).not.toHaveStyleRule('bottom', '0'); - expect(resizeHandleContainer).not.toHaveStyleRule('left', '0'); - expect(resizeHandleContainer).not.toHaveStyleRule('position', 'absolute'); - expect(resizeHandleContainer).not.toHaveStyleRule('right', '0'); - expect(resizeHandleContainer).not.toHaveStyleRule('top', '0'); - }); - - test('it renders', () => { - const wrapper = shallow( - } - height="100%" - id="test" - onResize={jest.fn()} - render={() => <>} - /> - ); - - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - describe('resize cursor styling', () => { - test('it does NOT apply the global-resize-cursor style to the body by default', () => { - mount( - - } - height="100%" - id="test" - onResize={jest.fn()} - render={() => <>} - /> - - ); - - expect(document.body.className).not.toContain(globalResizeCursorClassName); - }); - - describe('#addGlobalResizeCursorStyleToBody', () => { - test('it adds the global-resize-cursor style to the body', () => { - mount( - - } - height="100%" - id="test" - onResize={jest.fn()} - render={() => <>} - /> - - ); - - addGlobalResizeCursorStyleToBody(); - - expect(document.body.className).toContain(globalResizeCursorClassName); - }); - }); - - describe('#removeGlobalResizeCursorStyleFromBody', () => { - test('it removes the global-resize-cursor style from body', () => { - mount( - - } - height="100%" - id="test" - onResize={jest.fn()} - render={() => <>} - /> - - ); - - addGlobalResizeCursorStyleToBody(); - removeGlobalResizeCursorStyleFromBody(); - - expect(document.body.className).not.toContain(globalResizeCursorClassName); - }); - }); - - describe('#calculateDeltaX', () => { - test('it returns 0 when prevX isEqual 0', () => { - expect(calculateDeltaX({ prevX: 0, screenX: 189 })).toEqual(0); - }); - - test('it returns positive difference when screenX > prevX', () => { - expect(calculateDeltaX({ prevX: 10, screenX: 189 })).toEqual(179); - }); - - test('it returns negative difference when prevX > screenX ', () => { - expect(calculateDeltaX({ prevX: 199, screenX: 189 })).toEqual(-10); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx deleted file mode 100644 index eb3326c2f2cd0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx +++ /dev/null @@ -1,145 +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, { useEffect, useRef } from 'react'; -import { fromEvent, Observable, Subscription } from 'rxjs'; -import { concatMap, takeUntil } from 'rxjs/operators'; -import styled from 'styled-components'; - -export type OnResize = ({ delta, id }: { delta: number; id: string }) => void; - -export const resizeCursorStyle = 'col-resize'; -export const globalResizeCursorClassName = 'global-resize-cursor'; - -/** This polyfill is for Safari and IE-11 only. `movementX` is more accurate and "feels" better, so only use this function on Safari and IE-11 */ -export const calculateDeltaX = ({ prevX, screenX }: { prevX: number; screenX: number }) => - prevX !== 0 ? screenX - prevX : 0; - -const isSafari = /^((?!chrome|android|crios|fxios|Firefox).)*safari/i.test(navigator.userAgent); - -interface ResizeHandleContainerProps { - bottom?: string | number; - /** optionally provide a height style ResizeHandleContainer */ - height?: string; - left?: string | number; - positionAbsolute?: boolean; - right?: string | number; - top?: string | number; -} - -interface Props extends ResizeHandleContainerProps { - /** a (styled) resize handle */ - handle: React.ReactNode; - /** the `onResize` callback will be invoked with this id */ - id: string; - /** invoked when the handle is resized */ - onResize: OnResize; - /** The resizeable content to render */ - render: (isResizing: boolean) => React.ReactNode; -} - -const ResizeHandleContainer = styled.div` - bottom: ${({ positionAbsolute, bottom }) => positionAbsolute && bottom}; - cursor: ${resizeCursorStyle}; - height: ${({ height }) => height}; - left: ${({ positionAbsolute, left }) => positionAbsolute && left}; - position: ${({ positionAbsolute }) => positionAbsolute && 'absolute'}; - right: ${({ positionAbsolute, right }) => positionAbsolute && right}; - top: ${({ positionAbsolute, top }) => positionAbsolute && top}; - z-index: ${({ positionAbsolute, theme }) => positionAbsolute && theme.eui.euiZLevel1}; -`; -ResizeHandleContainer.displayName = 'ResizeHandleContainer'; - -export const addGlobalResizeCursorStyleToBody = () => { - document.body.classList.add(globalResizeCursorClassName); -}; - -export const removeGlobalResizeCursorStyleFromBody = () => { - document.body.classList.remove(globalResizeCursorClassName); -}; - -export const Resizeable = React.memo( - ({ bottom, handle, height, id, left, onResize, positionAbsolute, render, right, top }) => { - const drag$ = useRef | null>(null); - const dragEventTargets = useRef>([]); - const dragSubscription = useRef(null); - const prevX = useRef(0); - const ref = useRef(null); - const upSubscription = useRef(null); - const isResizingRef = useRef(false); - - const calculateDelta = (e: MouseEvent) => { - const deltaX = calculateDeltaX({ prevX: prevX.current, screenX: e.screenX }); - prevX.current = e.screenX; - return deltaX; - }; - useEffect(() => { - const move$ = fromEvent(document, 'mousemove'); - const down$ = fromEvent(ref.current!, 'mousedown'); - const up$ = fromEvent(document, 'mouseup'); - - drag$.current = down$.pipe(concatMap(() => move$.pipe(takeUntil(up$)))); - dragSubscription.current = - drag$.current && - drag$.current.subscribe(event => { - // We do a feature detection of event.movementX here and if it is missing - // we calculate the delta manually. Browsers IE-11 and Safari will call calculateDelta - const delta = - event.movementX == null || isSafari ? calculateDelta(event) : event.movementX; - if (!isResizingRef.current) { - isResizingRef.current = true; - } - onResize({ id, delta }); - if (event.target != null && event.target instanceof HTMLElement) { - const htmlElement: HTMLElement = event.target; - dragEventTargets.current = [ - ...dragEventTargets.current, - { htmlElement, prevCursor: htmlElement.style.cursor }, - ]; - htmlElement.style.cursor = resizeCursorStyle; - } - }); - - upSubscription.current = up$.subscribe(() => { - if (isResizingRef.current) { - dragEventTargets.current.reverse().forEach(eventTarget => { - eventTarget.htmlElement.style.cursor = eventTarget.prevCursor; - }); - dragEventTargets.current = []; - isResizingRef.current = false; - } - }); - return () => { - if (dragSubscription.current != null) { - dragSubscription.current.unsubscribe(); - } - if (upSubscription.current != null) { - upSubscription.current.unsubscribe(); - } - }; - }, []); - - return ( - <> - {render(isResizingRef.current)} - - {handle} - - - ); - } -); - -Resizeable.displayName = 'Resizeable'; diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx index 988bb13841fa5..1804093732861 100644 --- a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { globalNode, HookWrapper } from '../../mock'; import { useScrollToTop } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx index 95c68d0233c69..430cffa520c00 100644 --- a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx @@ -5,15 +5,14 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { SelectableText } from '.'; describe('SelectableText', () => { test('renders correctly against snapshot', () => { const wrapper = shallow({'You may select this text'}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it applies the user-select: text style', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx index de463f8e29f91..1fdcd8eee941f 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx @@ -5,7 +5,6 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -14,7 +13,7 @@ import { SkeletonRow } from './index'; describe('SkeletonRow', () => { test('it renders', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the correct number of cells if cellCount is specified', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx index 20aea3251d838..dce360877130e 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; interface RowProps { @@ -12,7 +12,7 @@ interface RowProps { rowPadding?: string; } -const Row = styled.div.attrs(({ rowHeight, rowPadding, theme }) => ({ +const RowComponent = styled.div.attrs(({ rowHeight, rowPadding, theme }) => ({ className: 'siemSkeletonRow', rowHeight: rowHeight || theme.eui.euiSizeXL, rowPadding: rowPadding || `${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.xs}`, @@ -22,6 +22,10 @@ const Row = styled.div.attrs(({ rowHeight, rowPadding, theme }) => ({ height: ${({ rowHeight }) => rowHeight}; padding: ${({ rowPadding }) => rowPadding}; `; +RowComponent.displayName = 'RowComponent'; + +const Row = React.memo(RowComponent); + Row.displayName = 'Row'; interface CellProps { @@ -29,7 +33,7 @@ interface CellProps { cellMargin?: string; } -const Cell = styled.div.attrs(({ cellColor, cellMargin, theme }) => ({ +const CellComponent = styled.div.attrs(({ cellColor, cellMargin, theme }) => ({ className: 'siemSkeletonRow__cell', cellColor: cellColor || theme.eui.euiColorLightestShade, cellMargin: cellMargin || theme.eui.gutterTypes.gutterSmall, @@ -42,6 +46,10 @@ const Cell = styled.div.attrs(({ cellColor, cellMargin, theme }) => ( margin-left: ${({ cellMargin }) => cellMargin}; } `; +CellComponent.displayName = 'CellComponent'; + +const Cell = React.memo(CellComponent); + Cell.displayName = 'Cell'; export interface SkeletonRowProps extends CellProps, RowProps { @@ -51,9 +59,14 @@ export interface SkeletonRowProps extends CellProps, RowProps { export const SkeletonRow = React.memo( ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding, style }) => { - const cells = [...Array(cellCount)].map((_, i) => ( - - )); + const cells = useMemo( + () => + [...Array(cellCount)].map( + (_, i) => , + [cellCount] + ), + [cellCount, cellColor, cellMargin] + ); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx index edf3dc08c7282..baeca10ee0fae 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { get, uniq } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DefaultDraggable } from '../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx index c437994145d63..3dee668d66a70 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx @@ -6,9 +6,8 @@ import numeral from '@elastic/numeral'; import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { asArrayIfExists } from '../../lib/helpers'; import { getMockNetflowData } from '../../mock'; @@ -101,7 +100,7 @@ describe('SourceDestination', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(
{getSourceDestinationInstance()}
); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders a destination label', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx index 0333181c3521c..c994c4b2cd519 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Network } from './network'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx index 4ec317737e72d..ea6ce4caa7270 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Ip } from '../ip'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx index cfacbb077856c..a0b86b3e9a133 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { uniq } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DirectionBadge } from '../direction'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx index 0675212591a66..005ebc14dcdcc 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { ArrowBody, ArrowHead } from '../arrows'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx index 463373b5894f1..60ab59c3796ff 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx @@ -5,7 +5,7 @@ */ import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { asArrayIfExists } from '../../lib/helpers'; import { getMockNetflowData } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx index ebc5beaa4e354..33159387214e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx @@ -6,7 +6,7 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty, isEqual, uniqWith } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME, Port } from '../port'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx index 53dade22351bf..d6a3ce4158734 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { SourceDestinationArrows } from './source_destination_arrows'; import { SourceDestinationIp } from './source_destination_ip'; diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index 098f54640e4b2..5ed750b519cbf 100644 --- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -105,7 +105,7 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = showInspect={false} >
{ ], ])('disable charts', wrapper => { test('it renders the default widget', () => { - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should render titles', () => { @@ -180,7 +181,7 @@ describe('Stat Items Component', () => { ); }); test('it renders the default widget', () => { - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should handle multiple titles', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx index 3424c05f32d63..155b219c04b92 100644 --- a/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx @@ -5,7 +5,6 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -15,7 +14,7 @@ describe('Subtitle', () => { test('it renders', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders one subtitle string item', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx index 013104da7c612..c5838fa283e17 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { useUiSetting$ } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx index d864d4306b8ef..17c7c0bac8fa2 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx @@ -10,9 +10,8 @@ import { getRowItemDraggable, OverflowFieldComponent, } from './helpers'; -import * as React from 'react'; +import React from 'react'; import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { TestProviders } from '../../mock'; import { getEmptyValue } from '../empty_value'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -29,7 +28,7 @@ describe('Table Helpers', () => { idPrefix: 'idPrefix', }); const wrapper = shallow({rowItem}); - expect(toJson(wrapper.find('DraggableWrapper'))).toMatchSnapshot(); + expect(wrapper.find('DraggableWrapper')).toMatchSnapshot(); }); test('it returns empty value when rowItem is undefined', () => { @@ -97,7 +96,7 @@ describe('Table Helpers', () => { idPrefix: 'idPrefix', }); const wrapper = shallow({rowItems}); - expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); + expect(wrapper.find('DragDropContext')).toMatchSnapshot(); }); test('it returns empty value when rowItems is undefined', () => { @@ -193,7 +192,7 @@ describe('Table Helpers', () => { test('it returns correctly against snapshot', () => { const rowItemOverflow = getRowItemOverflow(items, 'attrName', 1, 1); const wrapper = shallow(
{rowItemOverflow}
); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it does not show "more not shown" when maxOverflowItems are not exceeded', () => { @@ -215,7 +214,7 @@ describe('Table Helpers', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it does not truncates as per custom overflowLength value', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx index 6c793126efa72..c2dfda6a81ce4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx @@ -11,7 +11,7 @@ import { EuiGlobalToastListToast as Toast, } from '@elastic/eui'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx index a9628ebbd183f..9351fddd90dd5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx index 54b1fb0893c83..6cf14cd972d3e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { Note } from '../../../../lib/note'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 1b66a130c3550..dfea99ffd7091 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -1,20 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - - - + - - + - - - - + + + - - + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx index b57b343d614a8..64e8aa3c7e7b7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { OnColumnRemoved } from '../../../events'; import { EventsHeadingExtra, EventsLoading } from '../../../styles'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx index 15911f522032a..ccaeeff972a81 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx @@ -4,6 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { Draggable } from 'react-beautiful-dnd'; +import { Resizable, ResizeCallback } from 're-resizable'; +import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; +import { getDraggableFieldId, DRAG_TYPE_FIELD } from '../../../drag_and_drop/helpers'; +import { DraggableFieldBadge } from '../../../draggables/field_badge'; +import { OnColumnRemoved, OnColumnSorted, OnFilterChange, OnColumnResized } from '../../events'; +import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; +import { Sort } from '../sort'; +import { DraggingContainer } from './common/dragging_container'; + +import { Header } from './header'; import { ColumnId } from '../column_id'; export type ColumnHeaderType = 'not-filtered' | 'text-filter'; @@ -22,3 +34,90 @@ export interface ColumnHeader { type?: string; width: number; } + +interface ColumneHeaderProps { + draggableIndex: number; + header: ColumnHeader; + onColumnRemoved: OnColumnRemoved; + onColumnSorted: OnColumnSorted; + onColumnResized: OnColumnResized; + onFilterChange?: OnFilterChange; + sort: Sort; + timelineId: string; +} + +const ColumnHeaderComponent: React.FC = ({ + draggableIndex, + header, + timelineId, + onColumnRemoved, + onColumnResized, + onColumnSorted, + onFilterChange, + sort, +}) => { + const [isDragging, setIsDragging] = React.useState(false); + const handleResizeStop: ResizeCallback = (e, direction, ref, delta) => { + onColumnResized({ columnId: header.id, delta: delta.width }); + }; + + return ( + , + }} + onResizeStop={handleResizeStop} + > + + {(dragProvided, dragSnapshot) => ( + + {!dragSnapshot.isDragging ? ( + +
+ + ) : ( + + + + + + )} + + )} + + + ); +}; + +export const ColumnHeader = React.memo(ColumnHeaderComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx new file mode 100644 index 0000000000000..21aa17aa1c52c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FC, memo, useEffect } from 'react'; + +interface DraggingContainerProps { + children: JSX.Element; + onDragging: Function; +} + +const DraggingContainerComponent: FC = ({ children, onDragging }) => { + useEffect(() => { + onDragging(true); + + return () => onDragging(false); + }); + + return children; +}; + +export const DraggingContainer = memo(DraggingContainerComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx index 057f751f451ac..853c1ec24b703 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx @@ -5,7 +5,7 @@ */ import { EuiText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Pin } from '../../../../pin'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index 634db2dc52676..87a1035fc0739 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -6,7 +6,7 @@ import { EuiCheckbox, EuiSuperSelect } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import { getEventsSelectOptions } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx index d93983d7d4054..b9cfee395bafb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ColumnHeaderType } from '../column_header'; import { defaultHeaders } from '../default_headers'; @@ -24,7 +23,7 @@ describe('Filter', () => { }; const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('rendering', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx index c56322cc69e0c..0b5247e7da678 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx @@ -5,7 +5,7 @@ */ import { noop } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { OnFilterChange } from '../../../events'; import { ColumnHeader } from '../column_header'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap index 64c2b6ed10692..d30054ae1a3fe 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap @@ -1,14 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header renders correctly against snapshot 1`] = ` -} - id="@timestamp" - onResize={[Function]} - positionAbsolute={true} - render={[Function]} - right="-1px" - top={0} -/> + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx new file mode 100644 index 0000000000000..c38ae26050c93 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiToolTip } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React from 'react'; + +import { TruncatableText } from '../../../../truncatable_text'; +import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles'; +import { useTimelineContext } from '../../../timeline_context'; +import { Sort } from '../../sort'; +import { SortIndicator } from '../../sort/sort_indicator'; +import { ColumnHeader } from '../column_header'; +import { HeaderToolTipContent } from '../header_tooltip_content'; +import { getSortDirection } from './helpers'; + +interface HeaderContentProps { + children: React.ReactNode; + header: ColumnHeader; + isResizing: boolean; + onClick: () => void; + sort: Sort; +} + +const HeaderContentComponent: React.FC = ({ + children, + header, + isResizing, + onClick, + sort, +}) => { + const isLoading = useTimelineContext(); + + return ( + + {header.aggregatable ? ( + + + } + > + <>{header.label ?? header.id} + + + + + + ) : ( + + + } + > + <>{header.label ?? header.id} + + + + )} + + {children} + + ); +}; + +export const HeaderContent = React.memo(HeaderContentComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx index bfcf3cd639799..fab2e7ee872bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Direction } from '../../../../../graphql/types'; import { TestProviders } from '../../../../../mock'; @@ -33,14 +32,12 @@ describe('Header', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('rendering', () => { @@ -50,9 +47,7 @@ describe('Header', () => { @@ -75,9 +70,7 @@ describe('Header', () => { @@ -99,9 +92,7 @@ describe('Header', () => { @@ -127,9 +118,7 @@ describe('Header', () => { @@ -154,9 +143,7 @@ describe('Header', () => { @@ -182,9 +169,7 @@ describe('Header', () => { @@ -202,9 +187,7 @@ describe('Header', () => { @@ -222,9 +205,7 @@ describe('Header', () => { @@ -335,9 +316,7 @@ describe('Header', () => { @@ -358,9 +337,7 @@ describe('Header', () => { @@ -370,25 +347,4 @@ describe('Header', () => { expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); }); }); - - describe('setIsResizing', () => { - test('setIsResizing have been call when it renders actions', () => { - const mockSetIsResizing = jest.fn(); - mount( - - - - ); - - expect(mockSetIsResizing).toHaveBeenCalled(); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx index 311b4bfda60fe..c45b9ce425deb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx @@ -4,103 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; -import { OnResize, Resizeable } from '../../../../resize_handle'; -import { TruncatableText } from '../../../../truncatable_text'; -import { OnColumnRemoved, OnColumnResized, OnColumnSorted, OnFilterChange } from '../../../events'; -import { - EventsHeading, - EventsHeadingHandle, - EventsHeadingTitleButton, - EventsHeadingTitleSpan, -} from '../../../styles'; -import { useTimelineContext } from '../../../timeline_context'; +import { OnColumnRemoved, OnColumnSorted, OnFilterChange } from '../../../events'; import { Sort } from '../../sort'; -import { SortIndicator } from '../../sort/sort_indicator'; import { Actions } from '../actions'; import { ColumnHeader } from '../column_header'; import { Filter } from '../filter'; -import { HeaderToolTipContent } from '../header_tooltip_content'; -import { getNewSortDirectionOnClick, getSortDirection } from './helpers'; - -interface HeaderCompProps { - children: React.ReactNode; - header: ColumnHeader; - isResizing: boolean; - onClick: () => void; - sort: Sort; -} - -const HeaderComp = React.memo( - ({ children, header, isResizing, onClick, sort }) => { - const isLoading = useTimelineContext(); - - return ( - - {header.aggregatable ? ( - - - } - > - <>{header.label ?? header.id} - - - - - - ) : ( - - - } - > - <>{header.label ?? header.id} - - - - )} - - {children} - - ); - } -); -HeaderComp.displayName = 'HeaderComp'; +import { getNewSortDirectionOnClick } from './helpers'; +import { HeaderContent } from './header_content'; interface Props { header: ColumnHeader; onColumnRemoved: OnColumnRemoved; - onColumnResized: OnColumnResized; onColumnSorted: OnColumnSorted; onFilterChange?: OnFilterChange; - setIsResizing: (isResizing: boolean) => void; sort: Sort; timelineId: string; } -/** Renders a header */ -export const HeaderComponent = ({ +export const HeaderComponent: React.FC = ({ header, onColumnRemoved, - onColumnResized, onColumnSorted, onFilterChange = noop, - setIsResizing, sort, -}: Props) => { - const onClick = () => { +}) => { + const onClick = useCallback(() => { onColumnSorted!({ columnId: header.id, sortDirection: getNewSortDirectionOnClick({ @@ -108,41 +39,17 @@ export const HeaderComponent = ({ currentSort: sort, }), }); - }; - - const onResize: OnResize = ({ delta, id }) => { - onColumnResized({ columnId: id, delta }); - }; - - const renderActions = (isResizing: boolean) => { - setIsResizing(isResizing); - return ( - <> - - - - - - - ); - }; + }, [onColumnSorted, header, sort]); return ( - } - id={header.id} - onResize={onResize} - positionAbsolute - render={renderActions} - right="-1px" - top={0} - /> + <> + + + + + + ); }; -HeaderComponent.displayName = 'HeaderComponent'; - export const Header = React.memo(HeaderComponent); - -Header.displayName = 'Header'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx index da348590bd044..20c139ae1d050 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx @@ -5,9 +5,8 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { defaultHeaders } from '../../../../../mock'; import { ColumnHeader } from '../column_header'; @@ -89,6 +88,6 @@ describe('HeaderToolTipContent', () => { test('it renders the expected table content', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx index a63ec2bf840a6..5deb2c3e66376 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx @@ -6,7 +6,7 @@ import { EuiIcon } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { getIconFromType } from '../../../../event_details/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx index 7765b0360d35b..4b97dd7573a45 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../helpers'; import { defaultHeaders } from './default_headers'; @@ -18,14 +17,6 @@ import { useMountAppended } from '../../../../utils/use_mount_appended'; import { ColumnHeadersComponent } from '.'; -jest.mock('../../../resize_handle/is_resizing', () => ({ - ...jest.requireActual('../../../resize_handle/is_resizing'), - useIsContainerResizing: () => ({ - isResizing: true, - setIsResizing: jest.fn(), - }), -})); - describe('ColumnHeaders', () => { const mount = useMountAppended(); @@ -54,7 +45,7 @@ describe('ColumnHeaders', () => { toggleColumn={jest.fn()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the field browser', () => { @@ -118,37 +109,5 @@ describe('ColumnHeaders', () => { ).toContain(h.id); }); }); - - test('it disables dragging during a column resize', () => { - const wrapper = mount( - - - - ); - - defaultHeaders.forEach(h => { - expect( - wrapper - .find('[data-test-subj="draggable"]') - .first() - .prop('isDragDisabled') - ).toBe(true); - }); - }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx index 95a7ae52b0f23..953ffb4d4932b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx @@ -6,20 +6,13 @@ import { EuiCheckbox } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import * as React from 'react'; -import { Draggable, Droppable } from 'react-beautiful-dnd'; +import React from 'react'; +import { Droppable } from 'react-beautiful-dnd'; import { BrowserFields } from '../../../../containers/source'; -import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; -import { - DRAG_TYPE_FIELD, - droppableTimelineColumnsPrefix, - getDraggableFieldId, -} from '../../../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../../../draggables/field_badge'; +import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '../../../drag_and_drop/helpers'; import { StatefulFieldsBrowser } from '../../../fields_browser'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; -import { useIsContainerResizing } from '../../../resize_handle/is_resizing'; import { OnColumnRemoved, OnColumnResized, @@ -39,7 +32,6 @@ import { import { Sort } from '../sort'; import { ColumnHeader } from './column_header'; import { EventsSelect } from './events_select'; -import { Header } from './header'; interface Props { actionsColumnWidth: number; @@ -78,132 +70,86 @@ export const ColumnHeadersComponent = ({ sort, timelineId, toggleColumn, -}: Props) => { - const { isResizing, setIsResizing } = useIsContainerResizing(); - - return ( - - - - {showEventsSelect && ( - - - - - - )} - - {showSelectAllCheckbox && ( - - - ) => { - onSelectAll({ isSelected: event.currentTarget.checked }); - }} - /> - - - )} - +}: Props) => ( + + + + {showEventsSelect && ( - - + + + + )} + {showSelectAllCheckbox && ( + + + ) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }} /> - + )} + + + + + + - - {dropProvided => ( + + {(dropProvided, snapshot) => ( + <> - {columnHeaders.map((header, i) => ( - ( + - {(dragProvided, dragSnapshot) => ( - - {!dragSnapshot.isDragging ? ( - -
- - ) : ( - - - - )} - - )} - + draggableIndex={draggableIndex} + timelineId={timelineId} + header={header} + onColumnRemoved={onColumnRemoved} + onColumnSorted={onColumnSorted} + onFilterChange={onFilterChange} + onColumnResized={onColumnResized} + sort={sort} + /> ))} - )} - - - - ); -}; - -ColumnHeadersComponent.displayName = 'ColumnHeadersComponent'; + {dropProvided.placeholder} + + )} + + + +); export const ColumnHeaders = React.memo(ColumnHeadersComponent); - -ColumnHeaders.displayName = 'ColumnHeaders'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx index d919bdb0788a6..12ce3bb709242 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { RangePicker } from '.'; import { Ranges } from './ranges'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx index c221829f92d04..de21fdac6434a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx @@ -5,7 +5,7 @@ */ import { EuiSelect } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { OnRangeSelected } from '../../../events'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx index c6b1ee056ebcc..4378a96b2919a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { DEFAULT_PLACEHOLDER, TextFilter } from '.'; @@ -14,7 +13,7 @@ describe('TextFilter', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('placeholder', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx index 09672eb9a38fe..fcc23314a1813 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx @@ -6,7 +6,7 @@ import { EuiFieldText } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { OnFilterChange } from '../../../events'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index d370a24cc1d4d..75c05dd1455af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Columns it renders the expected columns 1`] = ` - - - - - - + + - - - - - + + - - - - - + + - - - - - + + - - - - - + + - - - - - + + - - - - - + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx index f5b33296561c7..36427015260a7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { mockTimelineData } from '../../../../mock'; import { defaultHeaders } from '../column_headers/default_headers'; @@ -29,6 +28,6 @@ describe('Columns', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx index 2b2401519eb32..37b6e30215056 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TimelineNonEcsData } from '../../../../graphql/types'; import { OnColumnResized } from '../../events'; @@ -30,7 +30,7 @@ export const DataDrivenColumns = React.memo( return ( {columnHeaders.map((header, index) => ( - + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ columnName: header.id, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index baa5c35880d68..b93b0531c740f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -9,7 +9,7 @@ import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; import { BrowserFields } from '../../../../containers/source'; -import { TimelineDetailsComponentQuery } from '../../../../containers/timeline/details'; +import { TimelineDetailsQuery } from '../../../../containers/timeline/details'; import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../graphql/types'; import { requestIdleCallbackViaScheduler } from '../../../../lib/helpers/scheduler'; import { Note } from '../../../../lib/note'; @@ -91,7 +91,7 @@ interface AttributesProps { children: React.ReactNode; } -const Attributes = React.memo(({ children }) => { +const AttributesComponent: React.FC = ({ children }) => { const width = useTimelineWidthContext(); // Passing the styles directly to the component because the width is @@ -106,187 +106,187 @@ const Attributes = React.memo(({ children }) => { {children} ); -}); +}; -export const StatefulEvent = React.memo( - ({ - actionsColumnWidth, - addNoteToEvent, - browserFields, - columnHeaders, - columnRenderers, - event, - eventIdToNoteIds, - getNotesByIds, - isEventViewer = false, - isEventPinned = false, - loadingEventIds, - maxDelay = 0, - onColumnResized, - onPinEvent, - onRowSelected, - onUnPinEvent, - onUpdateColumns, - rowRenderers, - selectedEventIds, - showCheckboxes, - timelineId, - toggleColumn, - updateNote, - }) => { - const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); - const [initialRender, setInitialRender] = useState(false); - const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); +const Attributes = React.memo(AttributesComponent); - const divElement = useRef(null); +const StatefulEventComponent: React.FC = ({ + actionsColumnWidth, + addNoteToEvent, + browserFields, + columnHeaders, + columnRenderers, + event, + eventIdToNoteIds, + getNotesByIds, + isEventViewer = false, + isEventPinned = false, + loadingEventIds, + maxDelay = 0, + onColumnResized, + onPinEvent, + onRowSelected, + onUnPinEvent, + onUpdateColumns, + rowRenderers, + selectedEventIds, + showCheckboxes, + timelineId, + toggleColumn, + updateNote, +}) => { + const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); + const [initialRender, setInitialRender] = useState(false); + const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const onToggleShowNotes = useCallback(() => { - const eventId = event._id; - setShowNotes({ ...showNotes, [eventId]: !showNotes[eventId] }); - }, [event, showNotes]); + const divElement = useRef(null); - const onToggleExpanded = useCallback(() => { - const eventId = event._id; - setExpanded({ - ...expanded, - [eventId]: !expanded[eventId], - }); - }, [event, expanded]); + const onToggleShowNotes = useCallback(() => { + const eventId = event._id; + setShowNotes({ ...showNotes, [eventId]: !showNotes[eventId] }); + }, [event, showNotes]); - const associateNote = useCallback( - (noteId: string) => { - addNoteToEvent({ eventId: event._id, noteId }); - if (!isEventPinned) { - onPinEvent(event._id); // pin the event, because it has notes - } - }, - [addNoteToEvent, event, isEventPinned, onPinEvent] - ); + const onToggleExpanded = useCallback(() => { + const eventId = event._id; + setExpanded({ + ...expanded, + [eventId]: !expanded[eventId], + }); + }, [event, expanded]); - /** - * Incrementally loads the events when it mounts by trying to - * see if it resides within a window frame and if it is it will - * indicate to React that it should render its self by setting - * its initialRender to true. - */ - useEffect(() => { - let _isMounted = true; + const associateNote = useCallback( + (noteId: string) => { + addNoteToEvent({ eventId: event._id, noteId }); + if (!isEventPinned) { + onPinEvent(event._id); // pin the event, because it has notes + } + }, + [addNoteToEvent, event, isEventPinned, onPinEvent] + ); - requestIdleCallbackViaScheduler( - () => { - if (!initialRender && _isMounted) { - setInitialRender(true); - } - }, - { timeout: maxDelay } - ); - return () => { - _isMounted = false; - }; - }, []); + /** + * Incrementally loads the events when it mounts by trying to + * see if it resides within a window frame and if it is it will + * indicate to React that it should render its self by setting + * its initialRender to true. + */ + useEffect(() => { + let _isMounted = true; - // Number of current columns plus one for actions. - const columnCount = columnHeaders.length + 1; + requestIdleCallbackViaScheduler( + () => { + if (!initialRender && _isMounted) { + setInitialRender(true); + } + }, + { timeout: maxDelay } + ); + return () => { + _isMounted = false; + }; + }, []); - // If we are not ready to render yet, just return null - // see useEffect() for when it schedules the first - // time this stateful component should be rendered. - if (!initialRender) { - return ; - } + // Number of current columns plus one for actions. + const columnCount = columnHeaders.length + 1; - return ( - - {({ isVisible }) => { - if (isVisible) { - return ( - - {({ detailsData, loading }) => ( - { - if (c != null) { - divElement.current = c; - } - }} - > - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - children: ( - - ), - timelineId, - })} + // If we are not ready to render yet, just return null + // see useEffect() for when it schedules the first + // time this stateful component should be rendered. + if (!initialRender) { + return ; + } - - + {({ isVisible }) => { + if (isVisible) { + return ( + + {({ detailsData, loading }) => ( + { + if (c != null) { + divElement.current = c; + } + }} + > + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + children: ( + - - - )} - - ); - } else { - // Height place holder for visibility detection as well as re-rendering sections. - const height = - divElement.current != null - ? `${divElement.current.clientHeight}px` - : DEFAULT_ROW_HEIGHT; + ), + timelineId, + })} - // height is being inlined directly in here because of performance with StyledComponents - // involving quick and constant changes to the DOM. - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ; - } - }} - - ); - } -); + + + + + )} + + ); + } else { + // Height place holder for visibility detection as well as re-rendering sections. + const height = + divElement.current != null + ? `${divElement.current.clientHeight}px` + : DEFAULT_ROW_HEIGHT; + + // height is being inlined directly in here because of performance with StyledComponents + // involving quick and constant changes to the DOM. + // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 + return ; + } + }} + + ); +}; -StatefulEvent.displayName = 'StatefulEvent'; +export const StatefulEvent = React.memo(StatefulEventComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx index 9ea1bbb1e8430..a39c254c61126 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import uuid from 'uuid'; import { TimelineNonEcsData } from '../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx index 5c6a0872ce340..239d8a9d77916 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../../containers/source/mock'; import { Direction } from '../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 23406f1b5f35f..c4ad532f76fc4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -116,7 +116,7 @@ export const Body = React.memo( `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap index b867e0a788449..a79a7ed23e3d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap @@ -2,24 +2,14 @@ exports[`get_column_renderer renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap index e2eaf1d872b26..563e356a689c8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap @@ -2,24 +2,14 @@ exports[`plain_column_renderer rendering renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx index ad904554e33ad..53a2054412440 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { useMountAppended } from '../../../../utils/use_mount_appended'; import { TestProviders } from '../../../../mock'; @@ -25,7 +24,7 @@ describe('Args', () => { processTitle="process-title-1" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns an empty string when both args and process title are undefined', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx index 8282931aa0579..22367ec879851 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx index 8d3a713369031..21cccc88f4fbc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -30,7 +29,7 @@ describe('GenericDetails', () => { timelineId="test" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns auditd if the data does contain auditd data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx index b60d89c857421..c25c656b75e41 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx index af4971faae295..fce0e1d645e16 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -31,7 +30,7 @@ describe('GenericFileDetails', () => { timelineId="test" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns auditd if the data does contain auditd data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx index be84696033d69..797361878e6c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiSpacer, IconType } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index f2cce8b16b5d6..b78d9261849cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -46,7 +45,7 @@ describe('GenericRowRenderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return false if not a auditd datum', () => { @@ -125,7 +124,7 @@ describe('GenericRowRenderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return false if not a auditd datum', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index 71d56d641037d..bcf464ab6da15 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { IconType } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx index a5b861be08e56..598769e854b42 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info'; @@ -26,7 +25,7 @@ describe('UserPrimarySecondary', () => { secondary="secondary-1" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should render user name only if that is all that is present', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx index bd350a599bb47..a54042d3de9d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx index 2240d83169e0d..a0a9977f5765e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx @@ -6,8 +6,7 @@ import { EuiFlexItem } from '@elastic/eui'; import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; @@ -32,7 +31,7 @@ describe('SessionUserHostWorkingDir', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders with just eventId and contextId', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx index 3c825e6c931be..6a6b55bb817c8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx index 07207c822ad29..a7c9d10e82a2f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx @@ -9,7 +9,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx index 752163901de2e..824e8c00de307 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx @@ -6,7 +6,7 @@ import { EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { Details } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx index d6c50460194a7..e12eacd73559d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx @@ -9,7 +9,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx index fd49395379e24..c7a08620bebbb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../../draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx index 04f7cd9b560a6..b31d01b8e94a0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; @@ -36,7 +35,7 @@ describe('empty_column_renderer', () => { timelineId: 'test', }); const wrapper = shallow({emptyColumn}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return isInstance true if source is empty', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx index 67554cc764486..7e2346ced8785 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +/* eslint-disable react/display-name */ + +import React from 'react'; import { TimelineNonEcsData } from '../../../../graphql/types'; import { DraggableWrapper, DragEffects } from '../../../drag_and_drop/draggable_wrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx index 77569f07a23c2..72b879d4ade78 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -9,7 +9,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx index 10f9c4ad9e545..35a88f52f05a3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx @@ -6,7 +6,7 @@ import { EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx index 45a824e034b15..4e522f6ed5c94 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx @@ -9,7 +9,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx index 185e9c9a8287a..c2c42ba0e4ddc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../../draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx index 21fbafb64d57f..4da236bfa34c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; import { useMountAppended } from '../../../../utils/use_mount_appended'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx index 9ea5f2cdd99fa..7671e3f0509a5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx index ff63d02acc37c..d800821f8d8a5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx index a1c43f3ecb163..e4871c6479c6b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx index 5c54e5be3374c..73f7b004ca3f7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; -import * as React from 'react'; -import { ThemeProvider } from 'styled-components'; +import React from 'react'; import { mockTimelineData, TestProviders } from '../../../../mock'; import { getEmptyValue } from '../../../empty_value'; @@ -21,31 +18,34 @@ import { HOST_NAME_FIELD_NAME } from './constants'; jest.mock('../../../../lib/kibana'); describe('Events', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const mount = useMountAppended(); test('renders correctly against snapshot', () => { const wrapper = shallow( - + + + ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper.find('FormattedFieldValue')).toMatchSnapshot(); }); test('it renders a localized date tooltip for a field type of date that has a valid timestamp', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').exists()).toEqual(true); @@ -53,13 +53,15 @@ describe('Events', () => { test('it does NOT render a localized date tooltip when field type is NOT date, even if it contains valid timestamp', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').exists()).toEqual(false); @@ -72,13 +74,15 @@ describe('Events', () => { }; const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').exists()).toEqual(false); @@ -86,13 +90,15 @@ describe('Events', () => { test('it renders the value for a non-date field when the field is populated', () => { const wrapper = mount( - + + + ); expect(wrapper.text()).toEqual('nginx'); @@ -100,7 +106,7 @@ describe('Events', () => { test('it renders placeholder text for a non-date field when the field is NOT populated', () => { const wrapper = mount( - + { fieldType="text" value={get('fake.field', mockTimelineData[0].ecs)} /> - + ); expect(wrapper.text()).toEqual(getEmptyValue()); @@ -116,14 +122,16 @@ describe('Events', () => { test('it renders tooltip for truncatable message when it exists', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="message-tool-tip"]').exists()).toEqual(true); @@ -179,53 +187,61 @@ describe('Events', () => { test('it renders a message text string', () => { const wrapper = mount( - + + + ); expect(wrapper.text()).toEqual('some message'); }); test('it renders truncatable message text when fieldName is message with truncate prop', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="truncatable-message"]').exists()).toEqual(true); }); test('it does NOT render the truncatable message style when fieldName is NOT message', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="truncatable-message"]').exists()).toEqual(false); }); test('it renders a hyperlink to the hosts details page when fieldName is host.name, and a hostname is provided', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="host-details-link"]').exists()).toEqual(true); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx index 7529c0623ef47..280de3683a11f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx @@ -5,9 +5,10 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { isNumber, isString } from 'lodash/fp'; -import * as React from 'react'; +import { isNumber, isString, isEmpty } from 'lodash/fp'; +import React from 'react'; +import { DefaultDraggable } from '../../../draggables'; import { Bytes, BYTES_FORMAT } from '../../../bytes'; import { Duration, EVENT_DURATION_FIELD_NAME } from '../../../duration'; import { getOrEmptyTagFromValue, getEmptyTagValue } from '../../../empty_value'; @@ -23,6 +24,9 @@ import { MESSAGE_FIELD_NAME, } from './constants'; +// simple black-list to prevent dragging and dropping fields such as message name +const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; + export const FormattedFieldValue = React.memo<{ contextId: string; eventId: string; @@ -42,7 +46,16 @@ export const FormattedFieldValue = React.memo<{ /> ); } else if (fieldType === DATE_FIELD_TYPE) { - return ; + return ( + + + + ); } else if (PORT_NAMES.some(portName => fieldName === portName)) { return ( @@ -55,11 +68,16 @@ export const FormattedFieldValue = React.memo<{ const hostname = `${value}`; return isString(value) && hostname.length > 0 ? ( - + - {value} + {value} - + ) : ( getEmptyTagValue() ); @@ -67,34 +85,46 @@ export const FormattedFieldValue = React.memo<{ return ( ); - } else if (fieldName === MESSAGE_FIELD_NAME && value != null && value !== '') { - return ( - <> - {truncate ? ( - - - - {fieldName} - - - {value} - - - } - > - <>{value} - - - ) : ( + } else if (columnNamesNotDraggable.includes(fieldName)) { + return truncate && !isEmpty(value) ? ( + + + + {fieldName} + + + {value} + + + } + > <>{value} - )} - + + + ) : ( + <>{value} ); } else { - return getOrEmptyTagFromValue(value); + const contentValue = getOrEmptyTagFromValue(value); + const content = truncate ? {contentValue} : contentValue; + + return ( + + {content} + + ); } }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx index d445ec2859e2c..25d5c71caf48a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { TimelineNonEcsData } from '../../../../graphql/types'; import { mockTimelineData } from '../../../../mock'; @@ -41,7 +40,7 @@ describe('get_column_renderer', () => { }); const wrapper = shallow({column}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should render event severity when dealing with data that is not suricata', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index bea525116021d..f367769b78f40 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../../../containers/source/mock'; import { Ecs } from '../../../../graphql/types'; @@ -44,7 +43,7 @@ describe('get_column_renderer', () => { }); const wrapper = shallow({row}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should render plain row data when it is a non suricata row', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx index 6c58b1ec6f35c..d84dfcc561882 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { mockTimelineData, TestProviders } from '../../../../mock'; @@ -24,7 +23,7 @@ describe('HostWorkingDir', () => { workingDirectory="[working-directory-123]" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders a hostname without a workingDirectory', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx index 0bdecfecd6c59..db49df30be473 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx index 0904c836c2f30..0990301b6e2b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx @@ -5,7 +5,7 @@ */ import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Ecs } from '../../../../graphql/types'; import { asArrayIfExists } from '../../../../lib/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index 6ba8f3f28dae8..68629a9a70058 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -38,7 +37,7 @@ describe('netflowRowRenderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('#isInstance', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 3ae154a14aaf5..754d6ad99b7fe 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx index 80ae10a48415c..684def7386da0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx index 7cb6c3704a238..1402743ef8a51 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx index 008885b5264c8..8a22307767a40 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx @@ -5,23 +5,17 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import moment from 'moment-timezone'; -import * as React from 'react'; +import React from 'react'; import { TimelineNonEcsData } from '../../../../graphql/types'; -import { defaultHeaders, mockFrameworks, mockTimelineData, TestProviders } from '../../../../mock'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../mock'; import { getEmptyValue } from '../../../empty_value'; import { useMountAppended } from '../../../../utils/use_mount_appended'; import { plainColumnRenderer } from './plain_column_renderer'; import { getValues, deleteItemIdx, findItem } from './helpers'; -jest.mock('../../../../lib/kibana'); - -const mockFramework = mockFrameworks.default_UTC; - describe('plain_column_renderer', () => { const mount = useMountAppended(); @@ -41,7 +35,7 @@ describe('plain_column_renderer', () => { timelineId: 'test', }); const wrapper = shallow({column}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return isInstance false if source is empty', () => { @@ -134,11 +128,7 @@ describe('plain_column_renderer', () => { {column} ); - expect(wrapper.text()).toEqual( - moment - .tz(getValues('@timestamp', mockDatum)![0], mockFramework.dateFormatTz!) - .format(mockFramework.dateFormat) - ); + expect(wrapper.text()).toEqual('Nov 5, 2018 @ 19:03:25.937'); }); test('should return an empty value if destination ip is empty', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx index 25a70502e3e45..70485c41f3b88 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx @@ -4,29 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isNumber } from 'lodash/fp'; import React from 'react'; import { TimelineNonEcsData } from '../../../../graphql/types'; -import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../empty_value'; -import { FormattedIp } from '../../../formatted_ip'; -import { IS_OPERATOR, DataProvider } from '../../data_providers/data_provider'; -import { Provider } from '../../data_providers/provider'; import { ColumnHeader } from '../column_headers/column_header'; import { ColumnRenderer } from './column_renderer'; -import { IP_FIELD_TYPE, MESSAGE_FIELD_NAME } from './constants'; import { FormattedFieldValue } from './formatted_field'; -import { parseQueryValue } from './parse_query_value'; import { parseValue } from './parse_value'; export const dataExistsAtColumn = (columnName: string, data: TimelineNonEcsData[]): boolean => data.findIndex(item => item.field === columnName) !== -1; -// simple black-list to prevent dragging and dropping fields such as message name -const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; - export const plainColumnRenderer: ColumnRenderer = { isInstance: (columnName: string, data: TimelineNonEcsData[]) => dataExistsAtColumn(columnName, data), @@ -47,91 +36,17 @@ export const plainColumnRenderer: ColumnRenderer = { values: string[] | undefined | null; }) => values != null - ? values.map(value => { - const itemDataProvider: DataProvider = { - enabled: true, - id: escapeDataProviderId( - `plain-column-renderer-data-provider-${timelineId}-${columnName}-${eventId}-${field.id}-${value}` - ), - name: `${columnName}: ${parseQueryValue(value)}`, - queryMatch: { - field: field.id, - value: parseQueryValue(value), - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - and: [], - }; - if (field.type === IP_FIELD_TYPE) { - // since ip fields may contain multiple IP addresses, return a FormattedIp here to avoid a "draggable of draggables" - return ( - - ); - } - - if (columnNamesNotDraggable.includes(columnName)) { - if (truncate) { - return ( - - ); - } else { - return ( - - ); - } - } - // note: we use a raw DraggableWrapper here instead of a DefaultDraggable, - // because we pass a width to enable text truncation, and we will show empty values - return ( - - snapshot.isDragging ? ( - - - - ) : ( - - ) - } - truncate={truncate} - /> - ); - }) + ? values.map(value => ( + + )) : getEmptyTagValue(), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx index 355f3fb248238..50ea7ca05b921 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -6,7 +6,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -30,7 +29,7 @@ describe('plain_row_renderer', () => { timelineId: 'test', }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should always return isInstance true', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx index 1eed05e6d2a7e..6725830c97d0a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import { RowRenderer } from './row_renderer'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx index ff1cb60db0d93..8cc7323ed358f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; @@ -28,7 +27,7 @@ describe('ProcessDraggable', () => { processPid={123} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns null if everything is null', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx index 29d6c0e7d59c2..35512c60629dd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx index cb0b40bdd8fca..b6696d38dc1c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../../../draggables'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 3f77726474c56..027aa0df8bcdd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { mockTimelineData } from '../../../../../mock'; @@ -26,7 +25,7 @@ describe('SuricataDetails', () => { timelineId="test" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns text if the data does contain suricata data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx index 35733e5e0b31b..17f5f236265ed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx @@ -6,7 +6,7 @@ import { EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx index 6fe6523180e58..dd773bb88ef68 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { ExternalLinkIcon } from '../../../../external_link_icon'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 66c9613c02995..170d17e8e279e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { Ecs } from '../../../../../graphql/types'; @@ -35,7 +34,7 @@ describe('suricata_row_renderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return false if not a suricata datum', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index 0bafe54b715fd..b227326551e01 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { get } from 'lodash/fp'; import React from 'react'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index 4eefb4b0bc8b9..beae16af558ed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; import { useMountAppended } from '../../../../../utils/use_mount_appended'; @@ -30,7 +29,7 @@ describe('SuricataSignature', () => { signature="ET SCAN ATTACK Hello" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 632e8ff35950e..2b9adfe21b120 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -5,7 +5,7 @@ */ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { DragEffects, DraggableWrapper } from '../../../../drag_and_drop/draggable_wrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx index e42a91b7d7972..4e4e1a0b7bf6f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { AuthSsh } from './auth_ssh'; @@ -21,7 +20,7 @@ describe('AuthSsh', () => { sshMethod="[ssh-method]" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns null if sshSignature and sshMethod are both null', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx index 60eab8381e98d..0ff2eec35314d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../../draggables'; import { TokensFlexItem } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx index 54f5b2f165287..19113d93f7cb0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -30,7 +29,7 @@ describe('SystemGenericDetails', () => { timelineId="test" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns system rendering if the data does contain system data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx index e3627d0ec918a..e1524c8e5aecb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx index d4260d9bd183a..cab7191c13aef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -30,7 +29,7 @@ describe('SystemGenericFileDetails', () => { timelineId="test" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns system rendering if the data does contain system data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx index 401c1c522ca60..c47d9603cbea2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index b2dbdb6b0e45c..5f809d595f1b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; @@ -49,8 +48,6 @@ import { } from './generic_row_renderer'; import * as i18n from './translations'; -jest.mock('../../../../../lib/kibana'); - describe('GenericRowRenderer', () => { const mount = useMountAppended(); @@ -77,7 +74,7 @@ describe('GenericRowRenderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return false if not a system datum', () => { @@ -141,7 +138,7 @@ describe('GenericRowRenderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return false if not a auditd datum', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx index f8930fdca7ba2..3e64248d39876 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { get } from 'lodash/fp'; import React from 'react'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx index 167abe2185bcc..100c8fbe5a988 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../../mock'; import { useMountAppended } from '../../../../../utils/use_mount_appended'; @@ -26,7 +25,7 @@ describe('Package', () => { packageVersion="package-version-123" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns null if all of the package information is null ', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx index d87639d2b8d6e..a28e850e2af96 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../../draggables'; import { TokensFlexItem } from '../helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx index 41a71f55cae19..73d1d5cb441ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx @@ -6,7 +6,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -34,7 +33,7 @@ describe('unknown_column_renderer', () => { timelineId: 'test', }); const wrapper = shallow({emptyColumn}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return isInstance true with a made up column name', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx index 662392078f38a..45b670acb569a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../../mock'; import { UserHostWorkingDir } from './user_host_working_dir'; @@ -27,7 +26,7 @@ describe('UserHostWorkingDir', () => { workingDirectory="[working-directory-123]" /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it returns null if userDomain, userName, hostName, and workingDirectory are all null', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx index 281cfd39bd9d2..d370afee2585f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { DraggableBadge } from '../../../draggables'; import { TokensFlexItem } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 73b90410bc803..7617a01acf1d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../mock'; @@ -26,7 +25,7 @@ describe('ZeekDetails', () => { /> ); - expect(toJson(wrapper.find('ZeekDetails'))).toMatchSnapshot(); + expect(wrapper.find('ZeekDetails')).toMatchSnapshot(); }); test('it returns zeek.connection if the data does contain zeek.connection data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx index 1a31e560d7810..d8561186b4546 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx @@ -5,7 +5,7 @@ */ import { EuiSpacer } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index b4fd9a978b7d8..4242308a55a64 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { Ecs } from '../../../../../graphql/types'; @@ -34,7 +33,7 @@ describe('zeek_row_renderer', () => { }); const wrapper = shallow({children}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('should return false if not a zeek datum', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index 83d1c4347f57c..fc528e33b5ab6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { get } from 'lodash/fp'; import React from 'react'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 4ef2bb89e05ca..c09bd6b7a356d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Ecs } from '../../../../../graphql/types'; import { mockTimelineData, TestProviders } from '../../../../../mock'; @@ -37,7 +36,7 @@ describe('ZeekSignature', () => { describe('rendering', () => { test('it renders the default Zeek', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 6a6ae4e4e7da5..72f58df5677e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -6,7 +6,7 @@ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { get } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Ecs } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx index cda1fe0844e00..db3e96a4e2650 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Direction } from '../../../../graphql/types'; @@ -16,7 +15,7 @@ describe('SortIndicator', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the sort indicator', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx index fc77bbd725704..74fb1e5e4034c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx @@ -5,7 +5,7 @@ */ import { EuiIcon } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { Direction } from '../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx index d67c6c9648a15..a88062d9093d7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../mock/test_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; @@ -36,7 +35,7 @@ describe('DataProviders', () => { show={true} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it should render a placeholder when there are zero data providers', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx index c249e263d1205..10586657b52a3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Empty } from './empty'; import { TestProviders } from '../../../mock/test_providers'; @@ -15,7 +14,7 @@ describe('Empty', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); const dropMessage = ['Drop', 'anything', 'highlighted', 'here']; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 87d45f6d3db17..a47fb932ed26c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -5,7 +5,7 @@ */ import { EuiBadge, EuiBadgeProps, EuiText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx index cce6dfc140375..525cc8e301d11 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx @@ -5,7 +5,7 @@ */ import { rgba } from 'polished'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.test.tsx index aa3f07cb1b17a..f0d7ca83fb391 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.test.tsx @@ -5,8 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../mock/test_providers'; @@ -17,7 +16,7 @@ describe('Provider', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the data provider', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx index 05dc7b7b84587..badc92d00c174 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexItem } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { AndOrBadge } from '../../and_or_badge'; import { BrowserFields } from '../../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 17457b900f3a9..1a1e8292b7e02 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -6,7 +6,7 @@ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx index c9454846c5548..d8076ac90e6b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../mock/test_providers'; import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; @@ -36,7 +35,7 @@ describe('Providers', () => { onToggleDataProviderExcluded={jest.fn()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the data providers', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 4d095485ef69d..bfe99f6920e66 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiFormHelpText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx index 8f5d91d8ce11f..1c5df9d220a62 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index 07b7741e5c152..b6ca4fe125c69 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -5,9 +5,8 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { TestProviders } from '../../../mock/test_providers'; @@ -41,7 +40,7 @@ describe('Footer Timeline Component', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the loading panel at the beginning ', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 4527e39128f89..5af7aff4f8795 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { Direction } from '../../../graphql/types'; import { mockIndexPattern } from '../../../mock'; @@ -44,7 +43,7 @@ describe('Header', () => { }} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the data providers', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index 814d25d9c718d..7e570d613ca5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -5,7 +5,7 @@ */ import { EuiCallOut } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { IIndexPattern } from 'src/plugins/data/public'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index 0e222f470f0d7..ae139c24d0176 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -18,7 +18,7 @@ import { EuiOverlayMask, EuiToolTip, } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import uuid from 'uuid'; import { Note } from '../../../lib/note'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index bc05204cc47fe..495b94f8c02e7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { mockGlobalState, apolloClientObservable } from '../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx index 8a78d04c88311..c804ccf658296 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { ActionCreator } from 'typescript-fsa'; @@ -33,18 +33,23 @@ export interface TimelineRefetchProps { type OwnProps = TimelineRefetchProps & TimelineRefetchDispatch; -const TimelineRefetchComponent = memo( - ({ id, inputId, inspect, loading, refetch, setTimelineQuery }) => { - useEffect(() => { - setTimelineQuery({ id, inputId, inspect, loading, refetch }); - }, [id, inputId, loading, refetch, inspect]); - - return null; - } -); +const TimelineRefetchComponent: React.FC = ({ + id, + inputId, + inspect, + loading, + refetch, + setTimelineQuery, +}) => { + useEffect(() => { + setTimelineQuery({ id, inputId, inspect, loading, refetch }); + }, [id, inputId, loading, refetch, inspect]); + + return null; +}; export const TimelineRefetch = compose>( connect(null, { setTimelineQuery: inputsActions.setQuery, }) -)(TimelineRefetchComponent); +)(React.memo(TimelineRefetchComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx index c9b1a3ced6e93..5db453988cbb8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx @@ -5,7 +5,7 @@ */ import { EuiSpacer, EuiText } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index b645202ab4c54..45eb7f85c809f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import { esFilters, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx new file mode 100644 index 0000000000000..c4361bbc8990d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -0,0 +1,322 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiHighlight, + EuiInputPopover, + EuiSuperSelect, + EuiSelectable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTextColor, + EuiFilterButton, + EuiFilterGroup, + EuiPortal, +} from '@elastic/eui'; +import { Option } from '@elastic/eui/src/components/selectable/types'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useMemo, useState } from 'react'; +import { ListProps } from 'react-virtualized'; +import styled, { createGlobalStyle } from 'styled-components'; + +import { AllTimelinesQuery } from '../../../containers/timeline/all'; +import { getEmptyTagValue } from '../../empty_value'; +import { isUntitled } from '../../../components/open_timeline/helpers'; +import * as i18nTimeline from '../../../components/open_timeline/translations'; +import { SortFieldTimeline, Direction } from '../../../graphql/types'; +import * as i18n from './translations'; + +const SearchTimelineSuperSelectGlobalStyle = createGlobalStyle` + .euiPopover__panel.euiPopover__panel-isOpen.timeline-search-super-select-popover__popoverPanel { + visibility: hidden; + z-index: 0; + } +`; + +const MyEuiFlexItem = styled(EuiFlexItem)` + display: inline-block; + max-width: 296px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const EuiSelectableContainer = styled.div` + .euiSelectable { + .euiFormControlLayout__childrenWrapper { + display: flex; + } + } +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + padding 0px 4px; +`; + +interface SearchTimelineSuperSelectProps { + isDisabled: boolean; + timelineId: string | null; + timelineTitle: string | null; + onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; +} + +const basicSuperSelectOptions = [ + { + value: '-1', + inputDisplay: i18n.DEFAULT_TIMELINE_TITLE, + }, +]; + +const getBasicSelectableOptions = (timelineId: string) => [ + { + description: i18n.DEFAULT_TIMELINE_DESCRIPTION, + label: i18n.DEFAULT_TIMELINE_TITLE, + id: null, + title: i18n.DEFAULT_TIMELINE_TITLE, + checked: timelineId === '-1' ? 'on' : undefined, + } as Option, +]; + +const ORIGINAL_PAGE_SIZE = 50; +const POPOVER_HEIGHT = 260; +const TIMELINE_ITEM_HEIGHT = 50; +const SearchTimelineSuperSelectComponent: React.FC = ({ + isDisabled, + timelineId, + timelineTitle, + onTimelineChange, +}) => { + const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); + const [heightTrigger, setHeightTrigger] = useState(0); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [searchTimelineValue, setSearchTimelineValue] = useState(''); + const [onlyFavorites, setOnlyFavorites] = useState(false); + const [searchRef, setSearchRef] = useState(null); + + const onSearchTimeline = useCallback(val => { + setSearchTimelineValue(val); + }, []); + + const handleClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const handleOpenPopover = useCallback(() => { + setIsPopoverOpen(true); + }, []); + + const handleOnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + const renderTimelineOption = useCallback((option, searchValue) => { + return ( + + + + + + + + + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} + + + + + + {option.description != null && option.description.trim().length > 0 + ? option.description + : getEmptyTagValue()} + + + + + + + + + + ); + }, []); + + const handleTimelineChange = useCallback( + options => { + const selectedTimeline = options.filter( + (option: { checked: string }) => option.checked === 'on' + ); + if (selectedTimeline != null && selectedTimeline.length > 0) { + onTimelineChange( + isEmpty(selectedTimeline[0].title) + ? i18nTimeline.UNTITLED_TIMELINE + : selectedTimeline[0].title, + selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id + ); + } + setIsPopoverOpen(false); + }, + [onTimelineChange] + ); + + const handleOnScroll = useCallback( + ( + totalTimelines: number, + totalCount: number, + { + clientHeight, + scrollHeight, + scrollTop, + }: { + clientHeight: number; + scrollHeight: number; + scrollTop: number; + } + ) => { + if (totalTimelines < totalCount) { + const clientHeightTrigger = clientHeight * 1.2; + if ( + scrollTop > 10 && + scrollHeight - scrollTop < clientHeightTrigger && + scrollHeight > heightTrigger + ) { + setHeightTrigger(scrollHeight); + setPageSize(pageSize + ORIGINAL_PAGE_SIZE); + } + } + }, + [heightTrigger, pageSize] + ); + + const superSelect = useMemo( + () => ( + + ), + [handleOpenPopover, isDisabled, timelineId, timelineTitle] + ); + + const favoritePortal = useMemo( + () => + searchRef != null ? ( + + + + + + {i18nTimeline.ONLY_FAVORITES} + + + + + + ) : null, + [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] + ); + + return ( + + + {({ timelines, loading, totalCount }) => ( + + { + setSearchRef(ref); + }, + }} + singleSelection={true} + options={[ + ...(!onlyFavorites && searchTimelineValue === '' + ? getBasicSelectableOptions(timelineId == null ? '-1' : timelineId) + : []), + ...timelines.map( + (t, index) => + ({ + description: t.description, + favorite: !isEmpty(t.favorite), + label: t.title, + id: t.savedObjectId, + key: `${t.title}-${index}`, + title: t.title, + checked: t.savedObjectId === timelineId ? 'on' : undefined, + } as Option) + ), + ]} + > + {(list, search) => ( + <> + {search} + {favoritePortal} + {list} + + )} + + + )} + + + + ); +}; + +export const SearchTimelineSuperSelect = memo(SearchTimelineSuperSelectComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/translations.ts new file mode 100644 index 0000000000000..bffee407bc999 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/translations.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 DEFAULT_TIMELINE_TITLE = i18n.translate('xpack.siem.timeline.defaultTimelineTitle', { + defaultMessage: 'Default blank timeline', +}); + +export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate( + 'xpack.siem.timeline.defaultTimelineDescription', + { + defaultMessage: 'Timeline offered by default when creating new timeline.', + } +); + +export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate( + 'xpack.siem.timeline.searchBoxPlaceholder', + { + defaultMessage: 'e.g. timeline name or description', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index db5d27626fc6d..b6fdc1b2973aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -27,10 +27,10 @@ export const TimelineBodyGlobalStyle = createGlobalStyle` } `; -export const TimelineBody = styled.div.attrs(({ className }) => ({ +export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ className: `siemTimeline__body ${className}`, }))<{ bodyHeight: number }>` - height: ${({ bodyHeight }) => bodyHeight + 'px'}; + height: ${({ bodyHeight }) => `${bodyHeight}px`}; overflow: auto; scrollbar-width: thin; @@ -56,15 +56,14 @@ TimelineBody.displayName = 'TimelineBody'; * EVENTS TABLE */ -export const EventsTable = styled.div.attrs(({ className }) => ({ +export const EventsTable = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable ${className}`, role: 'table', }))``; -EventsTable.displayName = 'EventsTable'; /* EVENTS HEAD */ -export const EventsThead = styled.div.attrs(({ className }) => ({ +export const EventsThead = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__thead ${className}`, role: 'rowgroup', }))` @@ -75,7 +74,6 @@ export const EventsThead = styled.div.attrs(({ className }) => ({ top: 0; z-index: ${({ theme }) => theme.eui.euiZLevel1}; `; -EventsThead.displayName = 'EventsThead'; export const EventsTrHeader = styled.div.attrs(({ className }) => ({ className: `siemEventsTable__trHeader ${className}`, @@ -83,34 +81,36 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({ }))` display: flex; `; -EventsTrHeader.displayName = 'EventsTrHeader'; -export const EventsThGroupActions = styled.div.attrs(({ className }) => ({ +export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__thGroupActions ${className}`, }))<{ actionsColumnWidth: number; justifyContent: string }>` display: flex; - flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'}; + flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`}; justify-content: ${({ justifyContent }) => justifyContent}; min-width: 0; `; -EventsThGroupActions.displayName = 'EventsThGroupActions'; -export const EventsThGroupData = styled.div.attrs(({ className }) => ({ +export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__thGroupData ${className}`, -}))` +}))<{ isDragging?: boolean }>` display: flex; + + > div:hover .siemEventsHeading__handle { + display: ${({ isDragging }) => (isDragging ? 'none' : 'block')}; + opacity: 1; + visibility: visible; + } `; -EventsThGroupData.displayName = 'EventsThGroupData'; -export const EventsTh = styled.div.attrs(({ className }) => ({ +export const EventsTh = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__th ${className}`, role: 'columnheader', -}))<{ isDragging?: boolean; position?: string }>` +}))` align-items: center; display: flex; flex-shrink: 0; min-width: 0; - position: ${({ position }) => position}; .siemEventsTable__thGroupActions &:first-child:last-child { flex: 1; @@ -121,10 +121,18 @@ export const EventsTh = styled.div.attrs(({ className }) => ({ cursor: move; /* Fallback for IE11 */ cursor: grab; } + + > div:focus { + outline: 0; /* disable focus on Resizable element */ + } + + /* don't display Draggable placeholder */ + [data-rbd-placeholder-context-id] { + display: none !important; + } `; -EventsTh.displayName = 'EventsTh'; -export const EventsThContent = styled.div.attrs(({ className }) => ({ +export const EventsThContent = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__thContent ${className}`, }))<{ textAlign?: string }>` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; @@ -135,19 +143,17 @@ export const EventsThContent = styled.div.attrs(({ className }) => ({ text-align: ${({ textAlign }) => textAlign}; width: 100%; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ `; -EventsThContent.displayName = 'EventsThContent'; /* EVENTS BODY */ -export const EventsTbody = styled.div.attrs(({ className }) => ({ +export const EventsTbody = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__tbody ${className}`, role: 'rowgroup', }))` overflow-x: hidden; `; -EventsTbody.displayName = 'EventsTbody'; -export const EventsTrGroup = styled.div.attrs(({ className }) => ({ +export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trGroup ${className}`, }))<{ className?: string }>` border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid @@ -157,17 +163,15 @@ export const EventsTrGroup = styled.div.attrs(({ className }) => ({ background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; } `; -EventsTrGroup.displayName = 'EventsTrGroup'; -export const EventsTrData = styled.div.attrs(({ className }) => ({ +export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trData ${className}`, role: 'row', }))` display: flex; `; -EventsTrData.displayName = 'EventsTrData'; -export const EventsTrSupplement = styled.div.attrs(({ className }) => ({ +export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trSupplement ${className}`, }))<{ className: string }>` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; @@ -175,26 +179,23 @@ export const EventsTrSupplement = styled.div.attrs(({ className }) => ({ padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 ${({ theme }) => theme.eui.paddingSizes.xl}; `; -EventsTrSupplement.displayName = 'EventsTrSupplement'; -export const EventsTdGroupActions = styled.div.attrs(({ className }) => ({ +export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__tdGroupActions ${className}`, }))<{ actionsColumnWidth: number }>` display: flex; justify-content: space-between; - flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'}; + flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`}; min-width: 0; `; -EventsTdGroupActions.displayName = 'EventsTdGroupActions'; -export const EventsTdGroupData = styled.div.attrs(({ className }) => ({ +export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__tdGroupData ${className}`, }))` display: flex; `; -EventsTdGroupData.displayName = 'EventsTdGroupData'; -export const EventsTd = styled.div.attrs(({ className }) => ({ +export const EventsTd = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__td ${className}`, role: 'cell', }))` @@ -207,7 +208,6 @@ export const EventsTd = styled.div.attrs(({ className }) => ({ flex: 1; } `; -EventsTd.displayName = 'EventsTd'; export const EventsTdContent = styled.div.attrs(({ className }) => ({ className: `siemEventsTable__tdContent ${className}`, @@ -219,13 +219,12 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({ text-align: ${({ textAlign }) => textAlign}; width: 100%; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ `; -EventsTdContent.displayName = 'EventsTdContent'; /** * EVENTS HEADING */ -export const EventsHeading = styled.div.attrs(({ className }) => ({ +export const EventsHeading = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsHeading ${className}`, }))<{ isLoading: boolean }>` align-items: center; @@ -235,9 +234,8 @@ export const EventsHeading = styled.div.attrs(({ className }) => ({ cursor: ${({ isLoading }) => (isLoading ? 'wait' : 'grab')}; } `; -EventsHeading.displayName = 'EventsHeading'; -export const EventsHeadingTitleButton = styled.button.attrs(({ className }) => ({ +export const EventsHeadingTitleButton = styled.button.attrs(({ className = '' }) => ({ className: `siemEventsHeading__title siemEventsHeading__title--aggregatable ${className}`, type: 'button', }))` @@ -260,16 +258,14 @@ export const EventsHeadingTitleButton = styled.button.attrs(({ className }) => ( margin-left: ${({ theme }) => theme.eui.euiSizeXS}; } `; -EventsHeadingTitleButton.displayName = 'EventsHeadingTitleButton'; export const EventsHeadingTitleSpan = styled.span.attrs(({ className }) => ({ className: `siemEventsHeading__title siemEventsHeading__title--notAggregatable ${className}`, }))` min-width: 0; `; -EventsHeadingTitleSpan.displayName = 'EventsHeadingTitleSpan'; -export const EventsHeadingExtra = styled.div.attrs(({ className }) => ({ +export const EventsHeadingExtra = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsHeading__extra ${className}`, }))` margin-left: auto; @@ -285,9 +281,8 @@ export const EventsHeadingExtra = styled.div.attrs(({ className }) => ({ } } `; -EventsHeadingExtra.displayName = 'EventsHeadingExtra'; -export const EventsHeadingHandle = styled.div.attrs(({ className }) => ({ +export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsHeading__handle ${className}`, }))` background-color: ${({ theme }) => theme.eui.euiBorderColor}; @@ -297,17 +292,11 @@ export const EventsHeadingHandle = styled.div.attrs(({ className }) => ({ visibility: hidden; width: ${({ theme }) => theme.eui.euiBorderWidthThick}; - .siemEventsTable__thead:hover & { - opacity: 1; - visibility: visible; - } - &:hover { background-color: ${({ theme }) => theme.eui.euiColorPrimary}; cursor: col-resize; } `; -EventsHeadingHandle.displayName = 'EventsHeadingHandle'; /** * EVENTS LOADING diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index bb500de239da7..2971053bc5252 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { timelineQuery } from '../../containers/timeline/index.gql_query'; @@ -78,7 +77,7 @@ describe('Timeline', () => { toggleColumn={jest.fn()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders the timeline header', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 5646b26428bf8..e15c58d32425a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx index d3251e1d331e3..611452cc7ccd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx @@ -45,30 +45,35 @@ interface ManageTimelineContextProps { // todo we need to refactor this as more complex context/reducer with useReducer // to avoid so many Context, at least the separation of code is there now -export const ManageTimelineContext = memo( - ({ children, loading, width, type = initTimelineType }) => { - const [myLoading, setLoading] = useState(initTimelineContext); - const [myWidth, setWidth] = useState(initTimelineWidth); - const [myType, setType] = useState(initTimelineType); +const ManageTimelineContextComponent: React.FC = ({ + children, + loading, + width, + type = initTimelineType, +}) => { + const [myLoading, setLoading] = useState(initTimelineContext); + const [myWidth, setWidth] = useState(initTimelineWidth); + const [myType, setType] = useState(initTimelineType); - useEffect(() => { - setLoading(loading); - }, [loading]); + useEffect(() => { + setLoading(loading); + }, [loading]); - useEffect(() => { - setType(type); - }, [type]); + useEffect(() => { + setType(type); + }, [type]); - useEffect(() => { - setWidth(width); - }, [width]); + useEffect(() => { + setWidth(width); + }, [width]); - return ( - - - {children} - - - ); - } -); + return ( + + + {children} + + + ); +}; + +export const ManageTimelineContext = memo(ManageTimelineContextComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx b/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx index 3a7774298f6af..bfca035e891e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ModalAllErrors } from './modal_all_errors'; import { AppToast } from '.'; @@ -30,7 +29,7 @@ describe('Modal all errors', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders null when isShowing is negative', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx index 8c5a08fdf5e21..92868b23a3ccd 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx @@ -5,15 +5,14 @@ */ import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { TruncatableText } from '.'; describe('TruncatableText', () => { test('renders correctly against snapshot', () => { const wrapper = shallow({'Hiding in plain sight'}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it adds the hidden overflow style', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx index 63412302fedfb..67823bea9e170 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { HookWrapper } from '../../mock'; import { SiemPageName } from '../../pages/home/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx index a7e7729de2e27..e656ec3496d8d 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx @@ -19,16 +19,12 @@ import { dispatchUpdateTimeline } from '../open_timeline/helpers'; import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url'; import { makeMapStateToProps } from './helpers'; -export const UrlStateContainer = React.memo( - (props: UrlStateContainerPropTypes) => { - useUrlStateHooks(props); - return null; - }, - (prevProps, nextProps) => - prevProps.pathName === nextProps.pathName && isEqual(prevProps.urlState, nextProps.urlState) -); - -UrlStateContainer.displayName = 'UrlStateContainer'; +export const UrlStateContainer: React.FC = ( + props: UrlStateContainerPropTypes +) => { + useUrlStateHooks(props); + return null; +}; const mapDispatchToProps = (dispatch: Dispatch) => ({ setInitialStateFromUrl: dispatchSetInitialStateFromUrl(dispatch), @@ -39,13 +35,21 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ export const UrlStateRedux = compose>( connect(makeMapStateToProps, mapDispatchToProps) -)(UrlStateContainer); +)( + React.memo( + UrlStateContainer, + (prevProps, nextProps) => + prevProps.pathName === nextProps.pathName && isEqual(prevProps.urlState, nextProps.urlState) + ) +); -export const UseUrlState = React.memo(props => { +const UseUrlStateComponent: React.FC = props => { const [routeProps] = useRouteSpy(); const urlStateReduxProps: RouteSpyState & UrlStateProps = { ...routeProps, ...props, }; return ; -}); +}; + +export const UseUrlState = React.memo(UseUrlStateComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx index 705b2106be315..f673b77ea13c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx @@ -5,7 +5,7 @@ */ import { mount } from 'enzyme'; -import * as React from 'react'; +import React from 'react'; import { HookWrapper } from '../../mock/hook_wrapper'; import { SiemPageName } from '../../pages/home/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx index 5d73e9bcf8e71..788ea14f4bd22 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; @@ -21,7 +20,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); + expect(wrapper.find('WrapperPage')).toMatchSnapshot(); }); describe('restrict width', () => { @@ -34,7 +33,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); + expect(wrapper.find('WrapperPage')).toMatchSnapshot(); }); test('custom max width when restrictWidth is number', () => { @@ -46,7 +45,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); + expect(wrapper.find('WrapperPage')).toMatchSnapshot(); }); test('custom max width when restrictWidth is string', () => { @@ -58,7 +57,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); + expect(wrapper.find('WrapperPage')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index a3ee878e305b4..b69a8de29e047 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -15,25 +15,27 @@ import { NewRule, Rule, FetchRuleProps, + BasicFetchProps, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_PREPACKAGED_URL, +} from '../../../../common/constants'; /** * Add provided Rule * * @param rule to add - * @param kbnVersion current Kibana Version to use for headers * @param signal to cancel request */ -export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise => { +export const addRule = async ({ rule, signal }: AddRulesProps): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: rule.id != null ? 'PUT' : 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, body: JSON.stringify(rule), signal, @@ -49,7 +51,6 @@ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Prom * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) * @param pagination desired pagination options (e.g. page/perPage) * @param id if specified, will return specific rule if exists - * @param kbnVersion current Kibana Version to use for headers * @param signal to cancel request */ export const fetchRules = async ({ @@ -64,7 +65,6 @@ export const fetchRules = async ({ total: 0, }, id, - kbnVersion, signal, }: FetchRulesProps): Promise => { const queryParams = [ @@ -101,16 +101,14 @@ export const fetchRules = async ({ * Fetch a Rule by providing a Rule ID * * @param id Rule ID's (not rule_id) - * @param kbnVersion current Kibana Version to use for headers */ -export const fetchRuleById = async ({ id, kbnVersion, signal }: FetchRuleProps): Promise => { +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { method: 'GET', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, signal, }); @@ -124,21 +122,15 @@ export const fetchRuleById = async ({ id, kbnVersion, signal }: FetchRuleProps): * * @param ids array of Rule ID's (not rule_id) to enable/disable * @param enabled to enable or disable - * @param kbnVersion current Kibana Version to use for headers */ -export const enableRules = async ({ - ids, - enabled, - kbnVersion, -}: EnableRulesProps): Promise => { +export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => { const requests = ids.map(id => fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'PUT', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, body: JSON.stringify({ id, enabled }), }) @@ -155,9 +147,8 @@ export const enableRules = async ({ * Deletes provided Rule ID's * * @param ids array of Rule ID's (not rule_id) to delete - * @param kbnVersion current Kibana Version to use for headers */ -export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise => { +export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => { // TODO: Don't delete if immutable! const requests = ids.map(id => fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { @@ -165,8 +156,7 @@ export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promis credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, }) ); @@ -182,20 +172,15 @@ export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promis * Duplicates provided Rules * * @param rule to duplicate - * @param kbnVersion current Kibana Version to use for headers */ -export const duplicateRules = async ({ - rules, - kbnVersion, -}: DuplicateRulesProps): Promise => { +export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { const requests = rules.map(rule => fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, body: JSON.stringify({ ...rule, @@ -218,3 +203,22 @@ export const duplicateRules = async ({ responses.map>(response => response.json()) ); }; + +/** + * Create Prepackaged Rules + * + * @param signal AbortSignal for cancelling request + */ +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, { + method: 'PUT', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-xsrf': 'true', + }, + signal, + }); + await throwIfNotOk(response); + return true; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx index 82490991236de..ea03c34ec31ba 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx @@ -6,8 +6,6 @@ import { useEffect, useState, Dispatch } from 'react'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; @@ -26,7 +24,6 @@ export const usePersistRule = (): Return => { const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); useEffect(() => { @@ -37,7 +34,7 @@ export const usePersistRule = (): Return => { if (rule != null) { try { setIsLoading(true); - await persistRule({ rule, kbnVersion, signal: abortCtrl.signal }); + await persistRule({ rule, signal: abortCtrl.signal }); if (isSubscribed) { setIsSaved(true); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 0885e541cead5..a329d96d444aa 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -41,7 +41,6 @@ export type NewRule = t.TypeOf; export interface AddRulesProps { rule: NewRule; - kbnVersion: string; signal: AbortSignal; } @@ -71,8 +70,8 @@ export const RuleSchema = t.intersection([ risk_score: t.number, rule_id: t.string, severity: t.string, - type: t.string, tags: t.array(t.string), + type: t.string, to: t.string, threats: t.array(t.unknown), updated_at: t.string, @@ -80,6 +79,8 @@ export const RuleSchema = t.intersection([ }), t.partial({ saved_id: t.string, + timeline_id: t.string, + timeline_title: t.string, }), ]); @@ -98,7 +99,6 @@ export interface FetchRulesProps { pagination?: PaginationOptions; filterOptions?: FilterOptions; id?: string; - kbnVersion: string; signal: AbortSignal; } @@ -117,22 +117,22 @@ export interface FetchRulesResponse { export interface FetchRuleProps { id: string; - kbnVersion: string; signal: AbortSignal; } export interface EnableRulesProps { ids: string[]; enabled: boolean; - kbnVersion: string; } export interface DeleteRulesProps { ids: string[]; - kbnVersion: string; } export interface DuplicateRulesProps { rules: Rules; - kbnVersion: string; +} + +export interface BasicFetchProps { + signal: AbortSignal; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx index ad0b87385ee79..22ba86cd09f74 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx @@ -6,8 +6,6 @@ import { useEffect, useState } from 'react'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { fetchRuleById } from './api'; @@ -25,7 +23,6 @@ type Return = [boolean, Rule | null]; export const useRule = (id: string | undefined): Return => { const [rule, setRule] = useState(null); const [loading, setLoading] = useState(true); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); useEffect(() => { @@ -37,7 +34,6 @@ export const useRule = (id: string | undefined): Return => { setLoading(true); const ruleResponse = await fetchRuleById({ id: idToFetch, - kbnVersion, signal: abortCtrl.signal, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index 66285c804aa28..b49dd8d51d4f7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -6,8 +6,6 @@ import { useEffect, useState } from 'react'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; import { useStateToaster } from '../../../components/toasters'; import { fetchRules } from './api'; @@ -35,7 +33,6 @@ export const useRules = ( data: [], }); const [loading, setLoading] = useState(true); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); useEffect(() => { @@ -48,7 +45,6 @@ export const useRules = ( const fetchRulesResult = await fetchRules({ filterOptions, pagination, - kbnVersion, signal: abortCtrl.signal, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index e69bbfe1925fb..8754d73637e7c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -29,12 +29,10 @@ import { parseJsonFromBody } from '../../../utils/api'; * Fetch Signals by providing a query * * @param query String to match a dsl - * @param kbnVersion current Kibana Version to use for headers * @param signal AbortSignal for cancelling request */ export const fetchQuerySignals = async ({ query, - kbnVersion, signal, }: QuerySignals): Promise> => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, { @@ -42,10 +40,9 @@ export const fetchQuerySignals = async ({ credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, - body: query, + body: JSON.stringify(query), signal, }); await throwIfNotOk(response); @@ -58,13 +55,11 @@ export const fetchQuerySignals = async ({ * * @param query of signals to update * @param status to update to('open' / 'closed') - * @param kbnVersion current Kibana Version to use for headers * @param signal AbortSignal for cancelling request */ export const updateSignalStatus = async ({ query, status, - kbnVersion, signal, }: UpdateSignalStatusProps): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_SIGNALS_STATUS_URL}`, { @@ -72,8 +67,7 @@ export const updateSignalStatus = async ({ credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, body: JSON.stringify({ status, ...query }), signal, @@ -86,20 +80,15 @@ export const updateSignalStatus = async ({ /** * Fetch Signal Index * - * @param kbnVersion current Kibana Version to use for headers * @param signal AbortSignal for cancelling request */ -export const getSignalIndex = async ({ - kbnVersion, - signal, -}: BasicSignals): Promise => { +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { method: 'GET', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, signal, }); @@ -117,20 +106,15 @@ export const getSignalIndex = async ({ /** * Get User Privileges * - * @param kbnVersion current Kibana Version to use for headers * @param signal AbortSignal for cancelling request */ -export const getUserPrivilege = async ({ - kbnVersion, - signal, -}: BasicSignals): Promise => { +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PRIVILEGES_URL}`, { method: 'GET', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, signal, }); @@ -142,20 +126,15 @@ export const getUserPrivilege = async ({ /** * Create Signal Index if needed it * - * @param kbnVersion current Kibana Version to use for headers * @param signal AbortSignal for cancelling request */ -export const createSignalIndex = async ({ - kbnVersion, - signal, -}: BasicSignals): Promise => { +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, + 'kbn-xsrf': 'true', }, signal, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts index 5b5dc9e9699fe..2b8f54e5438df 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts @@ -12,3 +12,24 @@ export const SIGNAL_FETCH_FAILURE = i18n.translate( defaultMessage: 'Failed to query signals', } ); + +export const PRIVILEGE_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription', + { + defaultMessage: 'Failed to query signals', + } +); + +export const SIGNAL_GET_NAME_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription', + { + defaultMessage: 'Failed to get signal index name', + } +); + +export const SIGNAL_POST_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription', + { + defaultMessage: 'Failed to create signal index', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 118c2b367ca5b..34cb7684a0399 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -7,11 +7,10 @@ export * from './errors_types'; export interface BasicSignals { - kbnVersion: string; signal: AbortSignal; } export interface QuerySignals extends BasicSignals { - query: string; + query: object; } export interface SignalsResponse { @@ -19,7 +18,8 @@ export interface SignalsResponse { timeout: boolean; } -export interface SignalSearchResponse extends SignalsResponse { +export interface SignalSearchResponse + extends SignalsResponse { _shards: { total: number; successful: number; @@ -39,7 +39,6 @@ export interface SignalSearchResponse extend export interface UpdateSignalStatusProps { query: object; status: 'open' | 'closed'; - kbnVersion: string; signal?: AbortSignal; // TODO: implement cancelling } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index 6f897703059f7..792ff29ad2488 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -6,12 +6,18 @@ import { useEffect, useState } from 'react'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; -import { useUiSetting$ } from '../../../lib/kibana'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../../components/toasters'; import { getUserPrivilege } from './api'; +import * as i18n from './translations'; -type Return = [boolean, boolean | null, boolean | null]; - +interface Return { + loading: boolean; + isAuthenticated: boolean | null; + hasIndexManage: boolean | null; + hasManageApiKey: boolean | null; + hasIndexWrite: boolean | null; +} /** * Hook to get user privilege from * @@ -19,8 +25,10 @@ type Return = [boolean, boolean | null, boolean | null]; export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); const [isAuthenticated, setAuthenticated] = useState(null); - const [hasWrite, setHasWrite] = useState(null); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); + const [hasIndexManage, setHasIndexManage] = useState(null); + const [hasIndexWrite, setHasIndexWrite] = useState(null); + const [hasManageApiKey, setHasManageApiKey] = useState(null); + const [, dispatchToaster] = useStateToaster(); useEffect(() => { let isSubscribed = true; @@ -30,7 +38,6 @@ export const usePrivilegeUser = (): Return => { async function fetchData() { try { const privilege = await getUserPrivilege({ - kbnVersion, signal: abortCtrl.signal, }); @@ -38,13 +45,21 @@ export const usePrivilegeUser = (): Return => { setAuthenticated(privilege.isAuthenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; - setHasWrite(privilege.index[indexName].create_index); + setHasIndexManage(privilege.index[indexName].manage); + setHasIndexWrite(privilege.index[indexName].write); + setHasManageApiKey( + privilege.cluster.manage_security || + privilege.cluster.manage_api_key || + privilege.cluster.manage_own_api_key + ); } } } catch (error) { if (isSubscribed) { setAuthenticated(false); - setHasWrite(false); + setHasIndexManage(false); + setHasIndexWrite(false); + errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); } } if (isSubscribed) { @@ -59,5 +74,5 @@ export const usePrivilegeUser = (): Return => { }; }, []); - return [loading, isAuthenticated, hasWrite]; + return { loading, isAuthenticated, hasIndexManage, hasManageApiKey, hasIndexWrite }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx index 9501f1189a483..fa88a84fb1187 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx @@ -4,26 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; - -import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import React, { SetStateAction, useEffect, useState } from 'react'; import { fetchQuerySignals } from './api'; import { SignalSearchResponse } from './types'; -type Return = [boolean, SignalSearchResponse | null]; +type Return = [ + boolean, + SignalSearchResponse | null, + React.Dispatch> +]; /** * Hook for using to get a Signals from the Detection Engine API * - * @param query convert a dsl into string + * @param initialQuery query dsl object * */ -export const useQuerySignals = (query: string): Return => { +export const useQuerySignals = (initialQuery: object): Return => { + const [query, setQuery] = useState(initialQuery); const [signals, setSignals] = useState | null>(null); const [loading, setLoading] = useState(true); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); useEffect(() => { let isSubscribed = true; @@ -34,7 +35,6 @@ export const useQuerySignals = (query: string): Return => try { const signalResponse = await fetchQuerySignals({ query, - kbnVersion, signal: abortCtrl.signal, }); @@ -58,5 +58,5 @@ export const useQuerySignals = (query: string): Return => }; }, [query]); - return [loading, signals]; + return [loading, signals, setQuery]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx index 347c90fa3b411..189d8a1bf3f75 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx @@ -6,13 +6,12 @@ import { useEffect, useState, useRef } from 'react'; -import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../../components/toasters'; -import { useUiSetting$ } from '../../../lib/kibana'; +import { createPrepackagedRules } from '../rules'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { PostSignalError } from './types'; +import { PostSignalError, SignalIndexError } from './types'; type Func = () => void; @@ -28,7 +27,6 @@ export const useSignalIndex = (): Return => { const [signalIndexName, setSignalIndexName] = useState(null); const [signalIndexExists, setSignalIndexExists] = useState(null); const createDeSignalIndex = useRef(null); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); useEffect(() => { @@ -38,19 +36,20 @@ export const useSignalIndex = (): Return => { const fetchData = async () => { try { setLoading(true); - const signal = await getSignalIndex({ - kbnVersion, - signal: abortCtrl.signal, - }); + const signal = await getSignalIndex({ signal: abortCtrl.signal }); if (isSubscribed && signal != null) { setSignalIndexName(signal.name); setSignalIndexExists(true); + createPrepackagedRules({ signal: abortCtrl.signal }); } } catch (error) { if (isSubscribed) { setSignalIndexName(null); setSignalIndexExists(false); + if (error instanceof SignalIndexError && error.statusCode !== 404) { + errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); + } } } if (isSubscribed) { @@ -62,10 +61,7 @@ export const useSignalIndex = (): Return => { let isFetchingData = false; try { setLoading(true); - await createSignalIndex({ - kbnVersion, - signal: abortCtrl.signal, - }); + await createSignalIndex({ signal: abortCtrl.signal }); if (isSubscribed) { isFetchingData = true; @@ -78,7 +74,7 @@ export const useSignalIndex = (): Return => { } else { setSignalIndexName(null); setSignalIndexExists(false); - errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster }); + errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster }); } } } diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx index 665148b7ad650..d77e0215f8353 100644 --- a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx @@ -47,33 +47,38 @@ interface OwnProps { type GlobalTimeProps = OwnProps & GlobalTimeReduxState & GlobalTimeDispatch; -export const GlobalTimeComponent = React.memo( - ({ children, deleteAllQuery, deleteOneQuery, from, to, setGlobalQuery }: GlobalTimeProps) => { - const [isInitializing, setIsInitializing] = useState(true); +export const GlobalTimeComponent: React.FC = ({ + children, + deleteAllQuery, + deleteOneQuery, + from, + to, + setGlobalQuery, +}) => { + const [isInitializing, setIsInitializing] = useState(true); - useEffect(() => { - if (isInitializing) { - setIsInitializing(false); - } - return () => { - deleteAllQuery({ id: 'global' }); - }; - }, []); + useEffect(() => { + if (isInitializing) { + setIsInitializing(false); + } + return () => { + deleteAllQuery({ id: 'global' }); + }; + }, []); - return ( - <> - {children({ - isInitializing, - from, - to, - setQuery: ({ id, inspect, loading, refetch }: SetQuery) => - setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), - deleteQuery: ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), - })} - - ); - } -); + return ( + <> + {children({ + isInitializing, + from, + to, + setQuery: ({ id, inspect, loading, refetch }: SetQuery) => + setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), + deleteQuery: ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + })} + + ); +}; const mapStateToProps = (state: State) => { const timerange: inputsModel.TimeRange = inputsSelectors.globalTimeRangeSelector(state); @@ -87,4 +92,4 @@ export const GlobalTime = connect(mapStateToProps, { deleteAllQuery: inputsActions.deleteAllQuery, deleteOneQuery: inputsActions.deleteOneQuery, setGlobalQuery: inputsActions.setQuery, -})(GlobalTimeComponent); +})(React.memo(GlobalTimeComponent)); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx index 5ff28480f1b3f..22c7b03f34dd5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx @@ -71,32 +71,38 @@ const getAllTimeline = memoizeOne( })) ); -export const AllTimelinesQuery = React.memo( - ({ children, onlyUserFavorite, pageInfo, search, sort }) => { - const variables: GetAllTimeline.Variables = { - onlyUserFavorite, - pageInfo, - search, - sort, - }; - return ( - - query={allTimelinesQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading }) => { - return children!({ - loading, - totalCount: getOr(0, 'getAllTimeline.totalCount', data), - timelines: getAllTimeline( - JSON.stringify(variables), - getOr([], 'getAllTimeline.timeline', data) - ), - }); - }} - - ); - } -); +const AllTimelinesQueryComponent: React.FC = ({ + children, + onlyUserFavorite, + pageInfo, + search, + sort, +}) => { + const variables: GetAllTimeline.Variables = { + onlyUserFavorite, + pageInfo, + search, + sort, + }; + return ( + + query={allTimelinesQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading }) => + children!({ + loading, + totalCount: getOr(0, 'getAllTimeline.totalCount', data), + timelines: getAllTimeline( + JSON.stringify(variables), + getOr([], 'getAllTimeline.timeline', data) + ), + }) + } + + ); +}; + +export const AllTimelinesQuery = React.memo(AllTimelinesQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx index 721cfefe01780..cf1b8954307e7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx @@ -32,33 +32,39 @@ const getDetailsEvent = memoizeOne( (variables: string, detail: DetailItem[]): DetailItem[] => detail ); -export const TimelineDetailsComponentQuery = React.memo( - ({ children, indexName, eventId, executeQuery, sourceId }) => { - const variables: GetTimelineDetailsQuery.Variables = { - sourceId, - indexName, - eventId, - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - }; - return executeQuery ? ( - - query={timelineDetailsQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => { - return children!({ - loading, - detailsData: getDetailsEvent( - JSON.stringify(variables), - getOr([], 'source.TimelineDetails.data', data) - ), - }); - }} - - ) : ( - children!({ loading: false, detailsData: null }) - ); - } -); +const TimelineDetailsQueryComponent: React.FC = ({ + children, + indexName, + eventId, + executeQuery, + sourceId, +}) => { + const variables: GetTimelineDetailsQuery.Variables = { + sourceId, + indexName, + eventId, + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + }; + return executeQuery ? ( + + query={timelineDetailsQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, refetch }) => + children!({ + loading, + detailsData: getDetailsEvent( + JSON.stringify(variables), + getOr([], 'source.TimelineDetails.data', data) + ), + }) + } + + ) : ( + children!({ loading: false, detailsData: null }) + ); +}; + +export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx index 12282241247cb..8d319ffe23902 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx @@ -17,13 +17,9 @@ const emptyIndexPattern: IndexPatternSavedObject[] = []; * * TODO: Refactor to context provider: https://github.com/elastic/siem-team/issues/448 * - * @param headers * @param signal */ -export const getIndexPatterns = async ( - signal: AbortSignal, - kbnVersion: string -): Promise => { +export const getIndexPatterns = async (signal: AbortSignal): Promise => { const response = await fetch( `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`, { @@ -31,9 +27,8 @@ export const getIndexPatterns = async ( credentials: 'same-origin', headers: { 'content-type': 'application/json', - 'kbn-xsrf': kbnVersion, - 'kbn-version': kbnVersion, 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', }, signal, } diff --git a/x-pack/legacy/plugins/siem/public/hooks/index.ts b/x-pack/legacy/plugins/siem/public/hooks/index.ts new file mode 100644 index 0000000000000..5049e4587d383 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/hooks/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { useDateFormat, useTimeZone } from './use_ui_settings'; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx index f5b595b0d01c6..7abe88402096c 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx @@ -6,11 +6,9 @@ import { useEffect, useState } from 'react'; -import { DEFAULT_KBN_VERSION } from '../../common/constants'; import { useStateToaster } from '../components/toasters'; import { errorToToaster } from '../components/ml/api/error_to_toaster'; import { IndexPatternSavedObject } from '../components/ml_popover/types'; -import { useUiSetting$ } from '../lib/kibana'; import { getIndexPatterns } from './api/api'; import * as i18n from './translations'; @@ -21,7 +19,6 @@ export const useIndexPatterns = (refreshToggle = false): Return => { const [indexPatterns, setIndexPatterns] = useState([]); const [isLoading, setIsLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); useEffect(() => { let isSubscribed = true; @@ -30,7 +27,7 @@ export const useIndexPatterns = (refreshToggle = false): Return => { async function fetchIndexPatterns() { try { - const data = await getIndexPatterns(abortCtrl.signal, kbnVersion); + const data = await getIndexPatterns(abortCtrl.signal); if (isSubscribed) { setIndexPatterns(data); diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_ui_settings.ts b/x-pack/legacy/plugins/siem/public/hooks/use_ui_settings.ts new file mode 100644 index 0000000000000..7eb0242e8e116 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/hooks/use_ui_settings.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; + +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../common/constants'; +import { useUiSetting } from '../lib/kibana'; + +export const useDateFormat = (): string => useUiSetting(DEFAULT_DATE_FORMAT); + +export const useTimeZone = (): string => { + const timeZone = useUiSetting(DEFAULT_DATE_FORMAT_TZ); + return timeZone === 'Browser' ? moment.tz.guess() : timeZone; +}; diff --git a/x-pack/legacy/plugins/siem/public/index.ts b/x-pack/legacy/plugins/siem/public/index.ts new file mode 100644 index 0000000000000..3a396a0637ea1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, PluginInitializerContext } from './plugin'; + +export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); diff --git a/x-pack/legacy/plugins/siem/public/legacy.ts b/x-pack/legacy/plugins/siem/public/legacy.ts new file mode 100644 index 0000000000000..49a03c93120d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/legacy.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npSetup, npStart } from 'ui/new_platform'; + +import { PluginInitializerContext } from '../../../../../src/core/public'; +import { plugin } from './'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +pluginInstance.setup(npSetup.core, npSetup.plugins); +pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx b/x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx index 44cf7502f3c5e..fdb6ed130a525 100644 --- a/x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx @@ -6,7 +6,7 @@ import { EuiGlobalToastListToast as Toast, EuiButtonIcon } from '@elastic/eui'; import copy from 'copy-to-clipboard'; -import * as React from 'react'; +import React from 'react'; import uuid from 'uuid'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx index c392c2511551f..ee94c2daa3fc4 100644 --- a/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Clipboard } from './clipboard'; diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts b/x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts index 80a42b91dbd32..9cdd4148134ad 100644 --- a/x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts @@ -20,9 +20,7 @@ export const getLinks = (cache: InMemoryCache) => [ }), new HttpLink({ credentials: 'same-origin', - headers: { - 'kbn-xsrf': chrome.getXsrfToken(), - }, + headers: { 'kbn-xsrf': 'true' }, uri: `${chrome.getBasePath()}/api/siem/graphql`, }), ]; diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/index.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/index.ts index 96d9c8330d265..012a1cfef5da2 100644 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/kibana/index.ts @@ -12,7 +12,7 @@ import { useUiSetting$, withKibana, } from '../../../../../../../src/plugins/kibana_react/public'; -import { StartServices } from '../../apps/plugin'; +import { StartServices } from '../../plugin'; export type KibanaContext = KibanaReactContextValue; export interface WithKibanaProps { diff --git a/x-pack/legacy/plugins/siem/public/lib/lib.ts b/x-pack/legacy/plugins/siem/public/lib/lib.ts index 7a6cd32aa8864..e7b39d2ea50f9 100644 --- a/x-pack/legacy/plugins/siem/public/lib/lib.ts +++ b/x-pack/legacy/plugins/siem/public/lib/lib.ts @@ -24,7 +24,6 @@ export interface AppFrameworkAdapter { darkMode?: boolean; indexPattern?: string; anomalyScore?: number; - kbnVersion?: string; scaledDateFormat?: string; timezone?: string; diff --git a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx b/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx index 4f1c2049c0027..292ddc036dcaf 100644 --- a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; interface HookWrapperProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/legacy/plugins/siem/public/mock/index.ts b/x-pack/legacy/plugins/siem/public/mock/index.ts index 620e266618c5c..dbf5f2e55e713 100644 --- a/x-pack/legacy/plugins/siem/public/mock/index.ts +++ b/x-pack/legacy/plugins/siem/public/mock/index.ts @@ -8,7 +8,6 @@ export * from './global_state'; export * from './header'; export * from './hook_wrapper'; export * from './index_pattern'; -export * from './kibana_config'; export * from './mock_timeline_data'; export * from './mock_detail_item'; export * from './netflow'; diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts deleted file mode 100644 index 23f1f0e86dd6a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts +++ /dev/null @@ -1,123 +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 { - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_BYTES_FORMAT, - DEFAULT_KBN_VERSION, - DEFAULT_TIMEZONE_BROWSER, - DEFAULT_TIMEPICKER_QUICK_RANGES, -} from '../../common/constants'; - -export interface MockFrameworks { - bytesFormat: string; - dateFormat: string; - dateFormatTz: string; - timezone: string; -} - -export const getMockKibanaUiSetting = (config: MockFrameworks) => (key: string) => { - if (key === DEFAULT_DATE_FORMAT) { - return [config.dateFormat]; - } else if (key === DEFAULT_DATE_FORMAT_TZ) { - return [config.dateFormatTz]; - } else if (key === DEFAULT_BYTES_FORMAT) { - return [config.bytesFormat]; - } else if (key === DEFAULT_KBN_VERSION) { - return ['8.0.0']; - } else if (key === DEFAULT_TIMEZONE_BROWSER) { - return config && config.timezone ? [config.timezone] : ['America/New_York']; - } else if (key === DEFAULT_TIMEPICKER_QUICK_RANGES) { - return [ - [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - { - from: 'now-15m', - to: 'now', - display: 'Last 15 minutes', - }, - { - from: 'now-30m', - to: 'now', - display: 'Last 30 minutes', - }, - { - from: 'now-1h', - to: 'now', - display: 'Last 1 hour', - }, - { - from: 'now-24h', - to: 'now', - display: 'Last 24 hours', - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - }, - ], - ]; - } - return [null]; -}; - -export const mockFrameworks: Readonly> = { - bytes_short: { - bytesFormat: '0b', - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - dateFormatTz: 'Browser', - timezone: 'America/Denver', - }, - default_browser: { - bytesFormat: '0,0.[0]b', - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - dateFormatTz: 'Browser', - timezone: 'America/Denver', - }, - default_ET: { - bytesFormat: '0,0.[0]b', - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - dateFormatTz: 'America/New_York', - timezone: 'America/New_York', - }, - default_MT: { - bytesFormat: '0,0.[0]b', - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - dateFormatTz: 'America/Denver', - timezone: 'America/Denver', - }, - default_UTC: { - bytesFormat: '0,0.[0]b', - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - dateFormatTz: 'UTC', - timezone: 'UTC', - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts index 15944df1822b3..7d843977d1f32 100644 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts +++ b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -20,7 +22,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, - DEFAULT_TIMEZONE_BROWSER, + DEFAULT_BYTES_FORMAT, } from '../../common/constants'; import { defaultIndexPattern } from '../../default_index_pattern'; import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; @@ -38,8 +40,8 @@ export const mockUiSettings: Record = { value: DEFAULT_INTERVAL_VALUE, }, [DEFAULT_INDEX_KEY]: defaultIndexPattern, + [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - [DEFAULT_TIMEZONE_BROWSER]: 'America/New_York', [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', [DEFAULT_DARK_MODE]: false, }; @@ -85,11 +87,15 @@ export const createWithKibanaMock = () => { export const createKibanaContextProviderMock = () => { const kibana = createUseKibanaMock()(); + const uiSettings = { + ...kibana.services.uiSettings, + get: createUseUiSettingMock(), + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any return ({ services, ...rest }: any) => React.createElement(KibanaContextProvider, { ...rest, - services: { ...kibana.services, ...services }, + services: { ...kibana.services, uiSettings, ...services }, }); }; diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index 6c0a85e3ef778..c7692755c1330 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -9,7 +9,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; -import * as React from 'react'; +import React from 'react'; import { ApolloProvider } from 'react-apollo'; import { DragDropContext, DropResult, ResponderProvided } from 'react-beautiful-dnd'; import { Provider as ReduxStoreProvider } from 'react-redux'; @@ -61,26 +61,33 @@ Object.defineProperty(window, 'localStorage', { const MockKibanaContextProvider = createKibanaContextProviderMock(); /** A utility for wrapping children in the providers required to run most tests */ -export const TestProviders = React.memo( - ({ children, store = createStore(state, apolloClientObservable), onDragEnd = jest.fn() }) => ( - - - - - ({ eui: euiDarkVars, darkMode: true })}> - {children} - - - - - - ) +const TestProvidersComponent: React.FC = ({ + children, + store = createStore(state, apolloClientObservable), + onDragEnd = jest.fn(), +}) => ( + + + + + ({ eui: euiDarkVars, darkMode: true })}> + {children} + + + + + ); -export const TestProviderWithoutDragAndDrop = React.memo( - ({ children, store = createStore(state, apolloClientObservable) }) => ( - - {children} - - ) +export const TestProviders = React.memo(TestProvidersComponent); + +const TestProviderWithoutDragAndDropComponent: React.FC = ({ + children, + store = createStore(state, apolloClientObservable), +}) => ( + + {children} + ); + +export const TestProviderWithoutDragAndDrop = React.memo(TestProviderWithoutDragAndDropComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx index 41dd50120f345..2ad37960d0423 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiIconTip, EuiLink, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx new file mode 100644 index 0000000000000..1950531998450 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.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. + */ + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const NoWriteSignalsCallOutComponent = () => { + const [showCallOut, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCallOut ? ( + +

{i18n.NO_WRITE_SIGNALS_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const NoWriteSignalsCallOut = memo(NoWriteSignalsCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts new file mode 100644 index 0000000000000..065d775e1dc6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_WRITE_SIGNALS_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.noWriteSignalsCallOutTitle', + { + defaultMessage: 'Signals index permissions required', + } +); + +export const NO_WRITE_SIGNALS_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.noWriteSignalsCallOutMsg', + { + defaultMessage: + 'You are currently missing the required permissions to update signals. Please contact your administrator for further assistance.', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.siem.detectionEngine.dismissNoWriteSignalButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index c0ed58daeca7f..d08e282a4c399 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -42,18 +42,13 @@ export const updateSignalStatusAction = async ({ status, setEventsLoading, setEventsDeleted, - kbnVersion, }: UpdateSignalStatusActionProps) => { try { setEventsLoading({ eventIds: signalIds, isLoading: true }); const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds); - await updateSignalStatus({ - query: queryObject, - status, - kbnVersion, - }); + await updateSignalStatus({ query: queryObject, status }); // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds: signalIds, isDeleted: true }); } catch (e) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index ea9a3ccef05b4..83b6ba690ec5b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React from 'react'; @@ -166,58 +168,66 @@ export const requiredFieldsForActions = [ ]; export const getSignalsActions = ({ + canUserCRUD, + hasIndexWrite, setEventsLoading, setEventsDeleted, createTimeline, status, - kbnVersion, }: { + canUserCRUD: boolean; + hasIndexWrite: boolean; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; createTimeline: CreateTimeline; status: 'open' | 'closed'; - kbnVersion: string; -}): TimelineAction[] => [ - { - getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( - - sendSignalsToTimelineAction({ createTimeline, data: [data] })} - iconType="tableDensityNormal" - aria-label="Next" - /> - - ), - id: 'sendSignalToTimeline', - width: 26, - }, - { - getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( - - - updateSignalStatusAction({ - signalIds: [eventId], - status, - setEventsLoading, - setEventsDeleted, - kbnVersion, - }) - } - iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} - aria-label="Next" - /> - - ), - id: 'updateSignalStatus', - width: 26, - }, -]; +}): TimelineAction[] => { + const actions = [ + { + getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( + + sendSignalsToTimelineAction({ createTimeline, data: [data] })} + iconType="tableDensityNormal" + aria-label="Next" + /> + + ), + id: 'sendSignalToTimeline', + width: 26, + }, + ]; + return canUserCRUD && hasIndexWrite + ? [ + ...actions, + { + getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( + + + updateSignalStatusAction({ + signalIds: [eventId], + status, + setEventsLoading, + setEventsDeleted, + }) + } + iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} + aria-label="Next" + /> + + ), + id: 'updateSignalStatus', + width: 26, + }, + ] + : actions; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index f9e80334a8882..d149eb700ad03 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -25,8 +26,7 @@ import { SignalFilterOption, SignalsTableFilterGroup, } from './signals_filter_group'; -import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../../common/constants'; +import { useKibana } from '../../../../lib/kibana'; import { defaultHeaders } from '../../../../components/timeline/body/column_headers/default_headers'; import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header'; import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query'; @@ -47,6 +47,8 @@ import { useFetchIndexPatterns } from '../../../../containers/detection_engine/r import { InputsRange } from '../../../../store/inputs/model'; import { Query } from '../../../../../../../../../src/plugins/data/common/query'; +import { HeaderSection } from '../../../../components/header_section'; + const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; interface ReduxProps { @@ -89,8 +91,11 @@ interface DispatchProps { } interface OwnProps { + canUserCRUD: boolean; defaultFilters?: esFilters.Filter[]; + hasIndexWrite: boolean; from: number; + loading: boolean; signalsIndex: string; to: number; } @@ -99,6 +104,7 @@ type SignalsTableComponentProps = OwnProps & ReduxProps & DispatchProps; export const SignalsTableComponent = React.memo( ({ + canUserCRUD, createTimeline, clearEventsDeleted, clearEventsLoading, @@ -107,7 +113,9 @@ export const SignalsTableComponent = React.memo( from, globalFilters, globalQuery, + hasIndexWrite, isSelectAllChecked, + loading, loadingEventIds, removeTimelineLinkTo, selectedEventIds, @@ -121,7 +129,6 @@ export const SignalsTableComponent = React.memo( const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const [{ browserFields, indexPatterns }] = useFetchIndexPatterns([signalsIndex]); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const kibana = useKibana(); const getGlobalQuery = useCallback(() => { @@ -208,7 +215,6 @@ export const SignalsTableComponent = React.memo( status, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, - kbnVersion, }); }, [ @@ -231,8 +237,10 @@ export const SignalsTableComponent = React.memo( (totalCount: number) => { return ( 0} clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} isFilteredToOpen={filterGroup === FILTER_OPEN} selectAll={selectAllCallback} selectedEventIds={selectedEventIds} @@ -244,6 +252,8 @@ export const SignalsTableComponent = React.memo( ); }, [ + canUserCRUD, + hasIndexWrite, clearSelectionCallback, filterGroup, loadingEventIds.length, @@ -257,13 +267,14 @@ export const SignalsTableComponent = React.memo( const additionalActions = useMemo( () => getSignalsActions({ + canUserCRUD, + hasIndexWrite, createTimeline: createTimelineCallback, setEventsLoading: setEventsLoadingCallback, setEventsDeleted: setEventsDeletedCallback, status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, - kbnVersion, }), - [createTimelineCallback, filterGroup, kbnVersion] + [canUserCRUD, createTimelineCallback, filterGroup] ); const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); @@ -283,11 +294,20 @@ export const SignalsTableComponent = React.memo( queryFields: requiredFieldsForActions, timelineActions: additionalActions, title: i18n.SIGNALS_TABLE_TITLE, - selectAll, + selectAll: canUserCRUD ? selectAll : false, }), - [additionalActions, selectAll] + [additionalActions, canUserCRUD, selectAll] ); + if (loading) { + return ( + + + + + ); + } + return ( void; - }) => { - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - - const onClickOpenFilterCallback = useCallback(() => { - setFilterGroup(FILTER_OPEN); - onFilterGroupChanged(FILTER_OPEN); - }, [setFilterGroup, onFilterGroupChanged]); - - const onClickCloseFilterCallback = useCallback(() => { - setFilterGroup(FILTER_CLOSED); - onFilterGroupChanged(FILTER_CLOSED); - }, [setFilterGroup, onFilterGroupChanged]); - - return ( - - - {i18n.OPEN_SIGNALS} - - - - {i18n.CLOSED_SIGNALS} - - - ); - } -); +interface Props { + onFilterGroupChanged: (filterGroup: SignalFilterOption) => void; +} + +const SignalsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }) => { + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + + const onClickOpenFilterCallback = useCallback(() => { + setFilterGroup(FILTER_OPEN); + onFilterGroupChanged(FILTER_OPEN); + }, [setFilterGroup, onFilterGroupChanged]); + + const onClickCloseFilterCallback = useCallback(() => { + setFilterGroup(FILTER_CLOSED); + onFilterGroupChanged(FILTER_CLOSED); + }, [setFilterGroup, onFilterGroupChanged]); + + return ( + + + {i18n.OPEN_SIGNALS} + + + + {i18n.CLOSED_SIGNALS} + + + ); +}; + +export const SignalsTableFilterGroup = React.memo(SignalsTableFilterGroupComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx index bbbc7728e36a5..b756b2eb75a7a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx @@ -11,6 +11,15 @@ import { TimelineNonEcsData } from '../../../../../graphql/types'; import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; +interface GetBatchItems { + areEventsLoading: boolean; + allEventsSelected: boolean; + selectedEventIds: Readonly>; + updateSignalsStatus: UpdateSignalsStatus; + sendSignalsToTimeline: SendSignalsToTimeline; + closePopover: () => void; + isFilteredToOpen: boolean; +} /** * Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel * @@ -22,15 +31,15 @@ import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; * @param closePopover * @param isFilteredToOpen currently selected filter options */ -export const getBatchItems = ( - areEventsLoading: boolean, - allEventsSelected: boolean, - selectedEventIds: Readonly>, - updateSignalsStatus: UpdateSignalsStatus, - sendSignalsToTimeline: SendSignalsToTimeline, - closePopover: () => void, - isFilteredToOpen: boolean -) => { +export const getBatchItems = ({ + areEventsLoading, + allEventsSelected, + selectedEventIds, + updateSignalsStatus, + sendSignalsToTimeline, + closePopover, + isFilteredToOpen, +}: GetBatchItems) => { const allDisabled = areEventsLoading || Object.keys(selectedEventIds).length === 0; const sendToTimelineDisabled = allEventsSelected || uniqueRuleCount(selectedEventIds) > 1; const filterString = isFilteredToOpen diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 72b250470d19b..e28fb3e06870e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -22,6 +22,8 @@ import { TimelineNonEcsData } from '../../../../../graphql/types'; import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; interface SignalsUtilityBarProps { + canUserCRUD: boolean; + hasIndexWrite: boolean; areEventsLoading: boolean; clearSelection: () => void; isFilteredToOpen: boolean; @@ -33,103 +35,103 @@ interface SignalsUtilityBarProps { updateSignalsStatus: UpdateSignalsStatus; } -export const SignalsUtilityBar = React.memo( - ({ - areEventsLoading, - clearSelection, - totalCount, - selectedEventIds, - isFilteredToOpen, - selectAll, - showClearSelection, - updateSignalsStatus, - sendSignalsToTimeline, - }) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); +const SignalsUtilityBarComponent: React.FC = ({ + canUserCRUD, + hasIndexWrite, + areEventsLoading, + clearSelection, + totalCount, + selectedEventIds, + isFilteredToOpen, + selectAll, + showClearSelection, + updateSignalsStatus, + sendSignalsToTimeline, +}) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [ - areEventsLoading, - selectedEventIds, - updateSignalsStatus, - sendSignalsToTimeline, - isFilteredToOpen, - ] - ); + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [ + areEventsLoading, + selectedEventIds, + updateSignalsStatus, + sendSignalsToTimeline, + isFilteredToOpen, + hasIndexWrite, + ] + ); - const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); - const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( - defaultNumberFormat - ); + const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); + const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( + defaultNumberFormat + ); - return ( - <> - - - - - {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} - - + return ( + <> + + + + {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + - - {totalCount > 0 && ( - <> - - {i18n.SELECTED_SIGNALS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - + + {canUserCRUD && hasIndexWrite && ( + <> + + {i18n.SELECTED_SIGNALS( + showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, + showClearSelection ? totalCount : Object.keys(selectedEventIds).length + )} + - - {i18n.BATCH_ACTIONS} - + + {i18n.BATCH_ACTIONS} + - { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} - > - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} - - - )} - - - - - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection - ); - } + { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }} + > + {showClearSelection + ? i18n.CLEAR_SELECTION + : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + + + )} + + + + + ); +}; + +export const SignalsUtilityBar = React.memo( + SignalsUtilityBarComponent, + (prevProps, nextProps) => + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.totalCount === nextProps.totalCount && + prevProps.showClearSelection === nextProps.showClearSelection ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts index b02b8eb0ef976..51bea27ec6a4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts @@ -31,7 +31,6 @@ export interface UpdateSignalStatusActionProps { status: 'open' | 'closed'; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - kbnVersion: string; } export type SendSignalsToTimeline = () => void; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx deleted file mode 100644 index 094f17922af1a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx +++ /dev/null @@ -1,39 +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 { EuiPanel, EuiSelect } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { memo } from 'react'; - -import { HeaderSection } from '../../../../components/header_section'; -import { HistogramSignals } from '../../../../components/page/detection_engine/histogram_signals'; - -export const sampleChartOptions = [ - { text: 'Risk scores', value: 'risk_scores' }, - { text: 'Severities', value: 'severities' }, - { text: 'Top destination IPs', value: 'destination_ips' }, - { text: 'Top event actions', value: 'event_actions' }, - { text: 'Top event categories', value: 'event_categories' }, - { text: 'Top host names', value: 'host_names' }, - { text: 'Top rule types', value: 'rule_types' }, - { text: 'Top rules', value: 'rules' }, - { text: 'Top source IPs', value: 'source_ips' }, - { text: 'Top users', value: 'users' }, -]; - -export const SignalsCharts = memo(() => ( - - - noop} - prepend="Stack by" - value={sampleChartOptions[0].value} - /> - - - - -)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts new file mode 100644 index 0000000000000..f329780b075e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as i18n from './translations'; +import { SignalsHistogramOption } from './types'; + +export const signalsHistogramOptions: SignalsHistogramOption[] = [ + { text: i18n.STACK_BY_RISK_SCORES, value: 'signal.rule.risk_score' }, + { text: i18n.STACK_BY_SEVERITIES, value: 'signal.rule.severity' }, + { text: i18n.STACK_BY_DESTINATION_IPS, value: 'destination.ip' }, + { text: i18n.STACK_BY_ACTIONS, value: 'event.action' }, + { text: i18n.STACK_BY_CATEGORIES, value: 'event.category' }, + { text: i18n.STACK_BY_HOST_NAMES, value: 'host.name' }, + { text: i18n.STACK_BY_RULE_TYPES, value: 'signal.rule.type' }, + { text: i18n.STACK_BY_RULE_NAMES, value: 'signal.rule.name' }, + { text: i18n.STACK_BY_SOURCE_IPS, value: 'source.ip' }, + { text: i18n.STACK_BY_USERS, value: 'user.name' }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx new file mode 100644 index 0000000000000..fda40f5f9fa5d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.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 { Position } from '@elastic/charts'; +import { EuiButton, EuiPanel, EuiSelect } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { memo, useCallback, useMemo, useState } from 'react'; + +import { HeaderSection } from '../../../../components/header_section'; +import { SignalsHistogram } from './signals_histogram'; + +import * as i18n from './translations'; +import { Query } from '../../../../../../../../../src/plugins/data/common/query'; +import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { SignalsHistogramOption, SignalsTotal } from './types'; +import { signalsHistogramOptions } from './config'; +import { getDetectionEngineUrl } from '../../../../components/link_to'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { useUiSetting$ } from '../../../../lib/kibana'; + +const defaultTotalSignalsObj: SignalsTotal = { + value: 0, + relation: 'eq', +}; + +interface SignalsHistogramPanelProps { + defaultStackByOption?: SignalsHistogramOption; + filters?: esFilters.Filter[]; + from: number; + query?: Query; + legendPosition?: Position; + loadingInitial?: boolean; + showLinkToSignals?: boolean; + showTotalSignalsCount?: boolean; + stackByOptions?: SignalsHistogramOption[]; + title?: string; + to: number; + updateDateRange: (min: number, max: number) => void; +} + +export const SignalsHistogramPanel = memo( + ({ + defaultStackByOption = signalsHistogramOptions[0], + filters, + query, + from, + legendPosition = 'bottom', + loadingInitial = false, + showLinkToSignals = false, + showTotalSignalsCount = false, + stackByOptions, + to, + title = i18n.HISTOGRAM_HEADER, + updateDateRange, + }) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const [totalSignalsObj, setTotalSignalsObj] = useState(defaultTotalSignalsObj); + const [selectedStackByOption, setSelectedStackByOption] = useState( + defaultStackByOption + ); + + const totalSignals = useMemo( + () => + i18n.SHOWING_SIGNALS( + numeral(totalSignalsObj.value).format(defaultNumberFormat), + totalSignalsObj.value, + totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' + ), + [totalSignalsObj] + ); + + const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => { + setSelectedStackByOption( + stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption + ); + }, []); + + return ( + + + {stackByOptions && ( + + )} + {showLinkToSignals && ( + {i18n.VIEW_SIGNALS} + )} + + + + + ); + } +); + +SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/helpers.tsx new file mode 100644 index 0000000000000..ed503e9872f0a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/helpers.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from '../types'; +import { SignalSearchResponse } from '../../../../../containers/detection_engine/signals/types'; +import * as i18n from '../translations'; + +export const formatSignalsData = ( + signalsData: SignalSearchResponse<{}, SignalsAggregation> | null +) => { + const groupBuckets: SignalsGroupBucket[] = + signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; + return groupBuckets.reduce((acc, { key: group, signals }) => { + const signalsBucket: SignalsBucket[] = signals.buckets ?? []; + + return [ + ...acc, + ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ + x: key, + y: doc_count, + g: group, + })), + ]; + }, []); +}; + +export const getSignalsHistogramQuery = ( + stackByField: string, + from: number, + to: number, + additionalFilters: Array<{ + bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; + }> +) => ({ + aggs: { + signalsByGrouping: { + terms: { + field: stackByField, + missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + signals: { + auto_date_histogram: { + field: '@timestamp', + buckets: 36, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx new file mode 100644 index 0000000000000..218fcc3a70f79 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Axis, + Chart, + getAxisId, + getSpecId, + HistogramBarSeries, + niceTimeFormatByDay, + Position, + Settings, + timeFormatter, +} from '@elastic/charts'; +import React, { useEffect, useMemo } from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import { useQuerySignals } from '../../../../../containers/detection_engine/signals/use_query'; +import { Query } from '../../../../../../../../../../src/plugins/data/common/query'; +import { esFilters, esQuery } from '../../../../../../../../../../src/plugins/data/common/es_query'; +import { SignalsAggregation, SignalsTotal } from '../types'; +import { formatSignalsData, getSignalsHistogramQuery } from './helpers'; +import { useTheme } from '../../../../../components/charts/common'; +import { useKibana } from '../../../../../lib/kibana'; + +interface HistogramSignalsProps { + filters?: esFilters.Filter[]; + from: number; + legendPosition?: Position; + loadingInitial: boolean; + query?: Query; + setTotalSignalsCount: React.Dispatch; + stackByField: string; + to: number; + updateDateRange: (min: number, max: number) => void; +} + +export const SignalsHistogram = React.memo( + ({ + to, + from, + query, + filters, + legendPosition = 'bottom', + loadingInitial, + setTotalSignalsCount, + stackByField, + updateDateRange, + }) => { + const [isLoadingSignals, signalsData, setQuery] = useQuerySignals<{}, SignalsAggregation>( + getSignalsHistogramQuery(stackByField, from, to, []) + ); + const theme = useTheme(); + const kibana = useKibana(); + + const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + + useEffect(() => { + setTotalSignalsCount( + signalsData?.hits.total ?? { + value: 0, + relation: 'eq', + } + ); + }, [signalsData]); + + useEffect(() => { + const converted = esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter(f => f.meta.disabled === false) ?? [], + { + ...esQuery.getEsQueryConfig(kibana.services.uiSettings), + dateFormatTZ: undefined, + } + ); + + setQuery( + getSignalsHistogramQuery(stackByField, from, to, !isEmpty(converted) ? [converted] : []) + ); + }, [stackByField, from, to, query, filters]); + + return ( + <> + {loadingInitial || isLoadingSignals ? ( + + ) : ( + + + + + + + + + + )} + + ); + } +); +SignalsHistogram.displayName = 'SignalsHistogram'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts new file mode 100644 index 0000000000000..0245b9968cc36 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STACK_BY_LABEL = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.stackByLabel', + { + defaultMessage: 'Stack by', + } +); + +export const STACK_BY_RISK_SCORES = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.riskScoresDropDown', + { + defaultMessage: 'Risk scores', + } +); + +export const STACK_BY_SEVERITIES = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.severitiesDropDown', + { + defaultMessage: 'Severities', + } +); + +export const STACK_BY_DESTINATION_IPS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.destinationIpsDropDown', + { + defaultMessage: 'Top destination IPs', + } +); + +export const STACK_BY_SOURCE_IPS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.sourceIpsDropDown', + { + defaultMessage: 'Top source IPs', + } +); + +export const STACK_BY_ACTIONS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventActionsDropDown', + { + defaultMessage: 'Top event actions', + } +); + +export const STACK_BY_CATEGORIES = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventCategoriesDropDown', + { + defaultMessage: 'Top event categories', + } +); + +export const STACK_BY_HOST_NAMES = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.hostNamesDropDown', + { + defaultMessage: 'Top host names', + } +); + +export const STACK_BY_RULE_TYPES = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.ruleTypesDropDown', + { + defaultMessage: 'Top rule types', + } +); + +export const STACK_BY_RULE_NAMES = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.rulesDropDown', + { + defaultMessage: 'Top rules', + } +); + +export const STACK_BY_USERS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.usersDropDown', + { + defaultMessage: 'Top users', + } +); + +export const HISTOGRAM_HEADER = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.headerTitle', + { + defaultMessage: 'Signal detection frequency', + } +); + +export const ALL_OTHERS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); + +export const VIEW_SIGNALS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.viewSignalsButtonLabel', + { + defaultMessage: 'View signals', + } +); + +export const SHOWING_SIGNALS = ( + totalSignalsFormatted: string, + totalSignals: number, + modifier: string +) => + i18n.translate('xpack.siem.detectionEngine.signals.histogram.showingSignalsTitle', { + values: { totalSignalsFormatted, totalSignals, modifier }, + defaultMessage: + 'Showing: {modifier}{totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts new file mode 100644 index 0000000000000..4eb10852450ad --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.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. + */ + +export interface SignalsHistogramOption { + text: string; + value: string; +} + +export interface HistogramData { + x: number; + y: number; + g: string; +} + +export interface SignalsAggregation { + signalsByGrouping: { + buckets: SignalsGroupBucket[]; + }; +} + +export interface SignalsBucket { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface SignalsGroupBucket { + key: string; + signals: { + buckets: SignalsBucket[]; + }; +} + +export interface SignalsTotal { + value: number; + relation: string; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx index a90022d4a34ce..fc1110e382847 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx @@ -9,7 +9,7 @@ import { FormattedRelative } from '@kbn/i18n/react'; import React, { useState, useEffect } from 'react'; import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; -import { buildlastSignalsQuery } from './query.dsl'; +import { buildLastSignalsQuery } from './query.dsl'; import { Aggs } from './types'; interface SignalInfo { @@ -26,14 +26,7 @@ export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { ); - let query = ''; - try { - query = JSON.stringify(buildlastSignalsQuery(ruleId)); - } catch { - query = ''; - } - - const [loading, signals] = useQuerySignals(query); + const [loading, signals] = useQuerySignals(buildLastSignalsQuery(ruleId)); useEffect(() => { if (signals != null) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts index 0b14aa17a9450..8cb07a4f8e6b5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const buildlastSignalsQuery = (ruleId: string | undefined | null) => { +export const buildLastSignalsQuery = (ruleId: string | undefined | null) => { const queryFilter = [ { bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx new file mode 100644 index 0000000000000..bbaccb7882484 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noop } from 'lodash/fp'; +import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react'; + +import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; +import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; +import { useKibana } from '../../../../lib/kibana'; + +export interface State { + canUserCRUD: boolean | null; + hasIndexManage: boolean | null; + hasIndexWrite: boolean | null; + hasManageApiKey: boolean | null; + isSignalIndexExists: boolean | null; + isAuthenticated: boolean | null; + loading: boolean; + signalIndexName: string | null; +} + +const initialState: State = { + canUserCRUD: null, + hasIndexManage: null, + hasIndexWrite: null, + hasManageApiKey: null, + isSignalIndexExists: null, + isAuthenticated: null, + loading: true, + signalIndexName: null, +}; + +export type Action = + | { type: 'updateLoading'; loading: boolean } + | { + type: 'updateHasManageApiKey'; + hasManageApiKey: boolean | null; + } + | { + type: 'updateHasIndexManage'; + hasIndexManage: boolean | null; + } + | { + type: 'updateHasIndexWrite'; + hasIndexWrite: boolean | null; + } + | { + type: 'updateIsSignalIndexExists'; + isSignalIndexExists: boolean | null; + } + | { + type: 'updateIsAuthenticated'; + isAuthenticated: boolean | null; + } + | { + type: 'updateCanUserCRUD'; + canUserCRUD: boolean | null; + } + | { + type: 'updateSignalIndexName'; + signalIndexName: string | null; + }; + +export const userInfoReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'updateLoading': { + return { + ...state, + loading: action.loading, + }; + } + case 'updateHasIndexManage': { + return { + ...state, + hasIndexManage: action.hasIndexManage, + }; + } + case 'updateHasIndexWrite': { + return { + ...state, + hasIndexWrite: action.hasIndexWrite, + }; + } + case 'updateHasManageApiKey': { + return { + ...state, + hasManageApiKey: action.hasManageApiKey, + }; + } + case 'updateIsSignalIndexExists': { + return { + ...state, + isSignalIndexExists: action.isSignalIndexExists, + }; + } + case 'updateIsAuthenticated': { + return { + ...state, + isAuthenticated: action.isAuthenticated, + }; + } + case 'updateCanUserCRUD': { + return { + ...state, + canUserCRUD: action.canUserCRUD, + }; + } + case 'updateSignalIndexName': { + return { + ...state, + signalIndexName: action.signalIndexName, + }; + } + default: + return state; + } +}; + +const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); + +const useUserData = () => useContext(StateUserInfoContext); + +interface ManageUserInfoProps { + children: React.ReactNode; +} + +export const ManageUserInfo = ({ children }: ManageUserInfoProps) => ( + + {children} + +); + +export const useUserInfo = (): State => { + const [ + { + canUserCRUD, + hasIndexManage, + hasIndexWrite, + hasManageApiKey, + isSignalIndexExists, + isAuthenticated, + loading, + signalIndexName, + }, + dispatch, + ] = useUserData(); + const { + loading: privilegeLoading, + isAuthenticated: isApiAuthenticated, + hasIndexManage: hasApiIndexManage, + hasIndexWrite: hasApiIndexWrite, + hasManageApiKey: hasApiManageApiKey, + } = usePrivilegeUser(); + const [ + indexNameLoading, + isApiSignalIndexExists, + apiSignalIndexName, + createSignalIndex, + ] = useSignalIndex(); + + const uiCapabilities = useKibana().services.application.capabilities; + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; + + useEffect(() => { + if (loading !== privilegeLoading || indexNameLoading) { + dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); + } + }, [loading, privilegeLoading, indexNameLoading]); + + useEffect(() => { + if (hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) { + dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage }); + } + }, [hasIndexManage, hasApiIndexManage]); + + useEffect(() => { + if (hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { + dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); + } + }, [hasIndexWrite, hasApiIndexWrite]); + + useEffect(() => { + if (hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) { + dispatch({ type: 'updateHasManageApiKey', hasManageApiKey: hasApiManageApiKey }); + } + }, [hasManageApiKey, hasApiManageApiKey]); + + useEffect(() => { + if (isSignalIndexExists !== isApiSignalIndexExists && isApiSignalIndexExists != null) { + dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists }); + } + }, [isSignalIndexExists, isApiSignalIndexExists]); + + useEffect(() => { + if (isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) { + dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated }); + } + }, [isAuthenticated, isApiAuthenticated]); + + useEffect(() => { + if (canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { + dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); + } + }, [canUserCRUD, capabilitiesCanUserCRUD]); + + useEffect(() => { + if (signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { + dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); + } + }, [signalIndexName, apiSignalIndexName]); + + useEffect(() => { + if ( + isAuthenticated && + hasIndexManage && + isSignalIndexExists != null && + !isSignalIndexExists && + createSignalIndex != null + ) { + createSignalIndex(); + } + }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexManage]); + + return { + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasIndexManage, + hasIndexWrite, + hasManageApiKey, + signalIndexName, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 8e5c3e9f13118..e638cf89e77bb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer, EuiPanel, EuiLoadingContent } from '@elastic/eui'; -import React from 'react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import React, { useCallback } from 'react'; import { StickyContainer } from 'react-sticky'; +import { connect } from 'react-redux'; +import { ActionCreator } from 'typescript-fsa'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { SiemSearchBar } from '../../components/search_bar'; @@ -16,26 +18,59 @@ import { GlobalTime } from '../../containers/global_time'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { SignalsTable } from './components/signals'; -import * as signalsI18n from './components/signals/translations'; -import { SignalsCharts } from './components/signals_chart'; +import { Query } from '../../../../../../../src/plugins/data/common/query'; +import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; +import { State } from '../../store'; +import { inputsSelectors } from '../../store/inputs'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { InputsModelId } from '../../store/inputs/constants'; +import { InputsRange } from '../../store/inputs/model'; import { useSignalInfo } from './components/signals_info'; +import { SignalsTable } from './components/signals'; +import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; +import { SignalsHistogramPanel } from './components/signals_histogram_panel'; +import { signalsHistogramOptions } from './components/signals_histogram_panel/config'; +import { useUserInfo } from './components/user_info'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; -import { HeaderSection } from '../../components/header_section'; -interface DetectionEngineComponentProps { - loading: boolean; - isSignalIndexExists: boolean | null; - isUserAuthenticated: boolean | null; - signalsIndex: string | null; +interface ReduxProps { + filters: esFilters.Filter[]; + query: Query; } -export const DetectionEngineComponent = React.memo( - ({ loading, isSignalIndexExists, isUserAuthenticated, signalsIndex }) => { +export interface DispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; +} + +type DetectionEngineComponentProps = ReduxProps & DispatchProps; + +const DetectionEngineComponent = React.memo( + ({ filters, query, setAbsoluteRangeDatePicker }) => { + const { + loading, + isSignalIndexExists, + isAuthenticated: isUserAuthenticated, + canUserCRUD, + signalIndexName, + hasIndexWrite, + } = useUserInfo(); + const [lastSignals] = useSignalInfo({}); + + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -54,6 +89,7 @@ export const DetectionEngineComponent = React.memo + {hasIndexWrite != null && !hasIndexWrite && } {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -61,7 +97,6 @@ export const DetectionEngineComponent = React.memo - - - - - {({ to, from }) => - !loading ? ( - isSignalIndexExists && ( - - ) - ) : ( - - - - - ) - } + {({ to, from }) => ( + <> + + + + + + + )} @@ -108,10 +152,28 @@ export const DetectionEngineComponent = React.memo - ); } ); DetectionEngineComponent.displayName = 'DetectionEngineComponent'; + +const makeMapStateToProps = () => { + const getGlobalInputs = inputsSelectors.globalSelector(); + return (state: State) => { + const globalInputs: InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + + return { + query, + filters, + }; + }; +}; + +export const DetectionEngine = connect(makeMapStateToProps, { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, +})(DetectionEngineComponent); + +DetectionEngine.displayName = 'DetectionEngine'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index e8a2c98a94a56..c4e83429aebdb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -4,70 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; -import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; - import { CreateRuleComponent } from './rules/create'; -import { DetectionEngineComponent } from './detection_engine'; +import { DetectionEngine } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; -import { RuleDetailsComponent } from './rules/details'; +import { RuleDetails } from './rules/details'; import { RulesComponent } from './rules'; +import { ManageUserInfo } from './components/user_info'; const detectionEnginePath = `/:pageName(detection-engine)`; type Props = Partial> & { url: string }; -export const DetectionEngineContainer = React.memo(() => { - const [privilegeLoading, isAuthenticated, hasWrite] = usePrivilegeUser(); - const [ - indexNameLoading, - isSignalIndexExists, - signalIndexName, - createSignalIndex, - ] = useSignalIndex(); - - useEffect(() => { - if ( - isAuthenticated && - hasWrite && - isSignalIndexExists != null && - !isSignalIndexExists && - createSignalIndex != null - ) { - createSignalIndex(); - } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasWrite]); - - return ( +export const DetectionEngineContainer = React.memo(() => ( + - + + + + + + + + + + + + + - {isSignalIndexExists && isAuthenticated && ( - <> - - - - - - - - - - - - - - )} - ( @@ -75,6 +43,6 @@ export const DetectionEngineContainer = React.memo(() => { )} /> - ); -}); + +)); DetectionEngineContainer.displayName = 'DetectionEngineContainer'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index 4de6136e9d3de..469745262d944 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -17,18 +17,14 @@ import { import { Action } from './reducer'; export const editRuleAction = (rule: Rule, history: H.History) => { - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/${rule.id}/edit`); + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); }; export const runRuleAction = () => {}; -export const duplicateRuleAction = async ( - rule: Rule, - dispatch: React.Dispatch, - kbnVersion: string -) => { +export const duplicateRuleAction = async (rule: Rule, dispatch: React.Dispatch) => { dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); - const duplicatedRule = await duplicateRules({ rules: [rule], kbnVersion }); + const duplicatedRule = await duplicateRules({ rules: [rule] }); dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id }); }; @@ -37,25 +33,20 @@ export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch< dispatch({ type: 'setExportPayload', exportPayload: rules }); }; -export const deleteRulesAction = async ( - ids: string[], - dispatch: React.Dispatch, - kbnVersion: string -) => { +export const deleteRulesAction = async (ids: string[], dispatch: React.Dispatch) => { dispatch({ type: 'updateLoading', ids, isLoading: true }); - const deletedRules = await deleteRules({ ids, kbnVersion }); + const deletedRules = await deleteRules({ ids }); dispatch({ type: 'deleteRules', rules: deletedRules }); }; export const enableRulesAction = async ( ids: string[], enabled: boolean, - dispatch: React.Dispatch, - kbnVersion: string + dispatch: React.Dispatch ) => { try { dispatch({ type: 'updateLoading', ids, isLoading: true }); - const updatedRules = await enableRules({ ids, enabled, kbnVersion }); + const updatedRules = await enableRules({ ids, enabled }); dispatch({ type: 'updateRules', rules: updatedRules }); } catch { // TODO Add error toast support to actions (and @throw jsdoc to api calls) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx index c8fb9d98fde6a..72d38454ad9bc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx @@ -14,8 +14,7 @@ import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actio export const getBatchItems = ( selectedState: TableData[], dispatch: React.Dispatch, - closePopover: () => void, - kbnVersion: string + closePopover: () => void ) => { const containsEnabled = selectedState.some(v => v.activate); const containsDisabled = selectedState.some(v => !v.activate); @@ -29,7 +28,7 @@ export const getBatchItems = ( onClick={async () => { closePopover(); const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id); - await enableRulesAction(deactivatedIds, true, dispatch, kbnVersion); + await enableRulesAction(deactivatedIds, true, dispatch); }} > {i18n.BATCH_ACTION_ACTIVATE_SELECTED} @@ -41,7 +40,7 @@ export const getBatchItems = ( onClick={async () => { closePopover(); const activatedIds = selectedState.filter(s => s.activate).map(s => s.id); - await enableRulesAction(activatedIds, false, dispatch, kbnVersion); + await enableRulesAction(activatedIds, false, dispatch); }} > {i18n.BATCH_ACTION_DEACTIVATE_SELECTED} @@ -78,8 +77,7 @@ export const getBatchItems = ( closePopover(); await deleteRulesAction( selectedState.map(({ sourceRule: { id } }) => id), - dispatch, - kbnVersion + dispatch ); }} > diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index ad5d210efa42d..95b9c9324894f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { EuiBadge, EuiHealth, @@ -31,7 +33,7 @@ import * as i18n from '../translations'; import { PreferenceFormattedDate } from '../../../../components/formatted_date'; import { RuleSwitch } from '../components/rule_switch'; -const getActions = (dispatch: React.Dispatch, kbnVersion: string, history: H.History) => [ +const getActions = (dispatch: React.Dispatch, history: H.History) => [ { description: i18n.EDIT_RULE_SETTINGS, icon: 'visControls', @@ -50,7 +52,7 @@ const getActions = (dispatch: React.Dispatch, kbnVersion: string, histor description: i18n.DUPLICATE_RULE, icon: 'copy', name: i18n.DUPLICATE_RULE, - onClick: (rowItem: TableData) => duplicateRuleAction(rowItem.sourceRule, dispatch, kbnVersion), + onClick: (rowItem: TableData) => duplicateRuleAction(rowItem.sourceRule, dispatch), }, { description: i18n.EXPORT_RULE, @@ -62,116 +64,125 @@ const getActions = (dispatch: React.Dispatch, kbnVersion: string, histor description: i18n.DELETE_RULE, icon: 'trash', name: i18n.DELETE_RULE, - onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, kbnVersion), + onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch), }, ]; +type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; + // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? export const getColumns = ( dispatch: React.Dispatch, - kbnVersion: string, - history: H.History -): Array | EuiTableActionsColumnType> => [ - { - field: 'rule', - name: i18n.COLUMN_RULE, - render: (value: TableData['rule']) => {value.name}, - truncateText: true, - width: '24%', - }, - { - field: 'method', - name: i18n.COLUMN_METHOD, - truncateText: true, - }, - { - field: 'severity', - name: i18n.COLUMN_SEVERITY, - render: (value: TableData['severity']) => ( - - {value} - - ), - truncateText: true, - }, - { - field: 'lastCompletedRun', - name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: TableData['lastCompletedRun']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - - ); + history: H.History, + hasNoPermissions: boolean +): RulesColumns[] => { + const cols: RulesColumns[] = [ + { + field: 'rule', + name: i18n.COLUMN_RULE, + render: (value: TableData['rule']) => {value.name}, + truncateText: true, + width: '24%', }, - sortable: true, - truncateText: true, - width: '16%', - }, - { - field: 'lastResponse', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: TableData['lastResponse']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} - - ); + { + field: 'method', + name: i18n.COLUMN_METHOD, + truncateText: true, }, - truncateText: true, - }, - { - field: 'tags', - name: i18n.COLUMN_TAGS, - render: (value: TableData['tags']) => ( -
- <> - {value.map((tag, i) => ( - - {tag} - - ))} - -
- ), - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'activate', - name: i18n.COLUMN_ACTIVATE, - render: (value: TableData['activate'], item: TableData) => ( - - ), - sortable: true, - width: '85px', - }, - { - actions: getActions(dispatch, kbnVersion, history), - width: '40px', - } as EuiTableActionsColumnType, -]; + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: TableData['severity']) => ( + + {value} + + ), + truncateText: true, + }, + { + field: 'lastCompletedRun', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: TableData['lastCompletedRun']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + ); + }, + sortable: true, + truncateText: true, + width: '16%', + }, + { + field: 'lastResponse', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: TableData['lastResponse']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <> + {value.type === 'Fail' ? ( + + {value.type} + + ) : ( + {value.type} + )} + + ); + }, + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: TableData['tags']) => ( +
+ <> + {value.map((tag, i) => ( + + {tag} + + ))} + +
+ ), + truncateText: true, + width: '20%', + }, + { + align: 'center', + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: TableData['activate'], item: TableData) => ( + + ), + sortable: true, + width: '85px', + }, + ]; + const actions: RulesColumns[] = [ + { + actions: getActions(dispatch, history), + width: '40px', + } as EuiTableActionsColumnType, + ]; + + return hasNoPermissions ? cols : [...cols, ...actions]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 1909b75a85835..f5d3955314242 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -13,7 +13,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] id: rule.id, rule_id: rule.rule_id, rule: { - href: `#/detection-engine/rules/${encodeURIComponent(rule.id)}`, + href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`, name: rule.name, status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index 442360bbf1484..e900058b6c53c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -11,7 +11,7 @@ import { EuiLoadingContent, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { useHistory } from 'react-router-dom'; import uuid from 'uuid'; @@ -31,8 +31,6 @@ import { getBatchItems } from './batch_actions'; import { EuiBasicTableOnChange, TableData } from '../types'; import { allRulesReducer, State } from './reducer'; import * as i18n from '../translations'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../../common/constants'; import { JSONDownloader } from '../components/json_downloader'; import { useStateToaster } from '../../../../components/toasters'; @@ -62,7 +60,11 @@ const initialState: State = { * * Delete * * Import/Export */ -export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => { +export const AllRules = React.memo<{ + hasNoPermissions: boolean; + importCompleteToggle: boolean; + loading: boolean; +}>(({ hasNoPermissions, importCompleteToggle, loading }) => { const [ { exportPayload, @@ -78,16 +80,13 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp const history = useHistory(); const [isInitialLoad, setIsInitialLoad] = useState(true); const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); const getBatchItemsPopoverContent = useCallback( (closePopover: () => void) => ( - + ), - [selectedItems, dispatch, kbnVersion] + [selectedItems, dispatch] ); useEffect(() => { @@ -116,6 +115,15 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp }); }, [rulesData]); + const euiBasicTableSelectionProps = useMemo( + () => ({ + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + }), + [] + ); + return ( <> (importComp {i18n.SELECTED_RULES(selectedItems.length)} - - {i18n.BATCH_ACTIONS} - + {!hasNoPermissions && ( + + {i18n.BATCH_ACTIONS} + + )} (importComp { @@ -209,14 +219,12 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp totalItemCount: pagination.total, pageSizeOptions: [5, 10, 20], }} - selection={{ - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), - }} sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} /> - {isLoading && } + {(isLoading || loading) && ( + + )} )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx index 66353a9613650..77a30b70705ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import React, { memo } from 'react'; +import React from 'react'; import { RuleStatusIcon, RuleStatusIconProps } from '../status_icon'; @@ -13,7 +13,7 @@ interface AccordionTitleProps extends RuleStatusIconProps { title: string; } -export const AccordionTitle = memo(({ name, title, type }) => ( +const AccordionTitleComponent: React.FC = ({ name, title, type }) => ( @@ -24,4 +24,6 @@ export const AccordionTitle = memo(({ name, title, type }) -)); +); + +export const AccordionTitle = React.memo(AccordionTitleComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index f090f6d97eaf9..b3cc81b5cdfcf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -4,9 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; +import styled from 'styled-components'; import * as RuleI18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; @@ -17,11 +26,27 @@ interface AddItemProps { dataTestSubj: string; idAria: string; isDisabled: boolean; + validate?: (args: unknown) => boolean; } -export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { +const MyEuiFormRow = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiText { + padding-right: 32px; + } + } +`; + +export const AddItem = ({ + addText, + dataTestSubj, + field, + idAria, + isDisabled, + validate, +}: AddItemProps) => { + const [showValidation, setShowValidation] = useState(false); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - // const [items, setItems] = useState(['']); const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1); const inputsRef = useRef([]); @@ -29,7 +54,8 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad const removeItem = useCallback( (index: number) => { const values = field.value as string[]; - field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; + field.setValue(newValues.length === 0 ? [''] : newValues); inputsRef.current = [ ...inputsRef.current.slice(0, index), ...inputsRef.current.slice(index + 1), @@ -46,11 +72,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad const addItem = useCallback(() => { const values = field.value as string[]; - if (!isEmpty(values) && values[values.length - 1]) { - field.setValue([...values, '']); - } else if (isEmpty(values)) { - field.setValue(['']); - } + field.setValue([...values, '']); }, [field]); const updateItem = useCallback( @@ -58,22 +80,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad event.persist(); const values = field.value as string[]; const value = event.target.value; - if (isEmpty(value)) { - field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); - inputsRef.current = [ - ...inputsRef.current.slice(0, index), - ...inputsRef.current.slice(index + 1), - ]; - setHaveBeenKeyboardDeleted(inputsRef.current.length - 1); - inputsRef.current = inputsRef.current.map((ref, i) => { - if (i >= index && inputsRef.current[index] != null) { - ref.value = 're-render'; - } - return ref; - }); - } else { - field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); - } + field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); }, [field] ); @@ -104,11 +111,11 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad const values = field.value as string[]; return ( - - + + setShowValidation(true)} + onChange={e => updateItem(e, index)} + fullWidth + {...euiFieldProps} + /> + + removeItem(index)} aria-label={RuleI18n.DELETE} /> - } - onChange={e => updateItem(e, index)} - compressed - fullWidth - {...euiFieldProps} - /> + + + {values.length - 1 !== index && }
); })} - + {addText} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_label.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_label.tsx index 15844f5012291..bc2cd39da44be 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_label.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_label.tsx @@ -16,7 +16,7 @@ interface Props { valueLabel?: string; } -export const FilterLabel = memo(({ filter, valueLabel }) => { +const FilterLabelComponent: React.FC = ({ filter, valueLabel }) => { const prefixText = filter.meta.negate ? ` ${i18n.translate('xpack.siem.detectionEngine.createRule.filterLabel.negatedFilterPrefix', { defaultMessage: 'NOT ', @@ -90,4 +90,6 @@ export const FilterLabel = memo(({ filter, valueLabel }) => { ); } -}); +}; + +export const FilterLabel = memo(FilterLabelComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx new file mode 100644 index 0000000000000..09d0c1131ea10 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLink, + EuiText, + EuiListGroup, +} from '@elastic/eui'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; + +import { FilterLabel } from './filter_label'; +import * as i18n from './translations'; +import { BuildQueryBarDescription, BuildThreatsDescription, ListItems } from './types'; + +const EuiBadgeWrap = styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +`; + +export const buildQueryBarDescription = ({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, +}: BuildQueryBarDescription): ListItems[] => { + let items: ListItems[] = []; + if (!isEmpty(filters)) { + filterManager.setFilters(filters); + items = [ + ...items, + { + title: <>{i18n.FILTERS_LABEL} , + description: ( + + {filterManager.getFilters().map((filter, index) => ( + + + {indexPatterns != null ? ( + + ) : ( + + )} + + + ))} + + ), + }, + ]; + } + if (!isEmpty(query.query)) { + items = [ + ...items, + { + title: <>{i18n.QUERY_LABEL} , + description: <>{query.query} , + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{i18n.SAVED_ID_LABEL} , + description: <>{savedId} , + }, + ]; + } + return items; +}; + +const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` + .euiFlexItem { + margin-bottom: 0px; + } +`; + +const MyEuiListGroup = styled(EuiListGroup)` + padding: 0px; + .euiListGroupItem__button { + padding: 0px; + } +`; + +export const buildThreatsDescription = ({ + label, + threats, +}: BuildThreatsDescription): ListItems[] => { + if (threats.length > 0) { + return [ + { + title: label, + description: ( + + {threats.map((threat, index) => { + const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); + return ( + + +
+ + {tactic != null ? tactic.text : ''} + +
+ { + const myTechnique = techniquesOptions.find(t => t.name === technique.name); + return { + label: myTechnique != null ? myTechnique.label : '', + href: technique.reference, + target: '_blank', + }; + })} + /> +
+
+ ); + })} +
+ ), + }, + ]; + } + return []; +}; + +export const buildStringArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + return [ + { + title: label, + description: ( + + {values.map((val: string) => + isEmpty(val) ? null : ( + + {val} + + ) + )} + + ), + }, + ]; + } + return []; +}; + +export const buildSeverityDescription = (label: string, value: string): ListItems[] => { + return [ + { + title: label, + description: ( + + {value} + + ), + }, + ]; +}; + +export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { + if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + return [ + { + title: label, + description: ( + ({ + label: val, + href: val, + iconType: 'link', + size: 'xs', + target: '_blank', + }))} + /> + ), + }, + ]; + } + return []; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 39c660a0079a6..af4f93c0fdbcd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -4,19 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiBadge, - EuiDescriptionList, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiTextArea, - EuiLink, - EuiText, - EuiListGroup, -} from '@elastic/eui'; +import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; -import React, { memo, ReactNode, useState } from 'react'; +import React, { memo, useState } from 'react'; import styled from 'styled-components'; import { @@ -25,13 +15,19 @@ import { FilterManager, Query, } from '../../../../../../../../../../src/plugins/data/public'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations'; import { useKibana } from '../../../../../lib/kibana'; -import { FilterLabel } from './filter_label'; -import { FormSchema } from '../shared_imports'; -import * as I18n from './translations'; - import { IMitreEnterpriseAttack } from '../../types'; -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import { FieldValueTimeline } from '../pick_timeline'; +import { FormSchema } from '../shared_imports'; +import { ListItems } from './types'; +import { + buildQueryBarDescription, + buildSeverityDescription, + buildStringArrayDescription, + buildThreatsDescription, + buildUrlsDescription, +} from './helpers'; interface StepRuleDescriptionProps { direction?: 'row' | 'column'; @@ -40,67 +36,44 @@ interface StepRuleDescriptionProps { schema: FormSchema; } -const EuiBadgeWrap = styled(EuiBadge)` - .euiBadge__text { - white-space: pre-wrap !important; - } -`; - const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>` ${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')}; `; -const MyEuiListGroup = styled(EuiListGroup)` - padding: 0px; - .euiListGroupItem__button { - padding: 0px; - } -`; - -const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` - .euiFlexItem { - margin-bottom: 0px; - } -`; - const MyEuiTextArea = styled(EuiTextArea)` max-width: 100%; height: 80px; `; -export const StepRuleDescription = memo( - ({ data, direction = 'row', indexPatterns, schema }) => { - const kibana = useKibana(); - const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); +const StepRuleDescriptionComponent: React.FC = ({ + data, + direction = 'row', + indexPatterns, + schema, +}) => { + const kibana = useKibana(); + const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const keys = Object.keys(schema); - const listItems = keys.reduce( - (acc: ListItems[], key: string) => [ - ...acc, - ...buildListItems(data, pick(key, schema), filterManager, indexPatterns), - ], - [] - ); - return ( - - {chunk(Math.ceil(listItems.length / 2), listItems).map((chunckListItems, index) => ( - - - - ))} - - ); - } -); + const keys = Object.keys(schema); + const listItems = keys.reduce( + (acc: ListItems[], key: string) => [ + ...acc, + ...buildListItems(data, pick(key, schema), filterManager, indexPatterns), + ], + [] + ); + return ( + + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( + + + + ))} + + ); +}; -interface ListItems { - title: NonNullable; - description: NonNullable; -} +export const StepRuleDescription = memo(StepRuleDescriptionComponent); const buildListItems = ( data: unknown, @@ -129,103 +102,23 @@ const getDescriptionItem = ( filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => { - if (field === 'useIndicesConfig') { - return []; - } else if (field === 'queryBar') { + if (field === 'queryBar') { const filters = get('queryBar.filters', value) as esFilters.Filter[]; const query = get('queryBar.query', value) as Query; const savedId = get('queryBar.saved_id', value); - let items: ListItems[] = []; - if (!isEmpty(filters)) { - filterManager.setFilters(filters); - items = [ - ...items, - { - title: <>{I18n.FILTERS_LABEL}, - description: ( - - {filterManager.getFilters().map((filter, index) => ( - - - {indexPatterns != null ? ( - - ) : ( - - )} - - - ))} - - ), - }, - ]; - } - if (!isEmpty(query.query)) { - items = [ - ...items, - { - title: <>{I18n.QUERY_LABEL}, - description: <>{query.query}, - }, - ]; - } - if (!isEmpty(savedId)) { - items = [ - ...items, - { - title: <>{I18n.SAVED_ID_LABEL}, - description: <>{savedId}, - }, - ]; - } - return items; + return buildQueryBarDescription({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, + }); } else if (field === 'threats') { const threats: IMitreEnterpriseAttack[] = get(field, value).filter( (threat: IMitreEnterpriseAttack) => threat.tactic.name !== 'none' ); - if (threats.length > 0) { - return [ - { - title: label, - description: ( - - {threats.map((threat, index) => { - const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); - return ( - - -
- - {tactic != null ? tactic.text : ''} - -
- { - const myTechnique = techniquesOptions.find( - t => t.name === technique.name - ); - return { - label: myTechnique != null ? myTechnique.label : '', - href: technique.reference, - target: '_blank', - }; - })} - /> -
-
- ); - })} -
- ), - }, - ]; - } - return []; + return buildThreatsDescription({ label, threats }); } else if (field === 'description') { return [ { @@ -233,27 +126,31 @@ const getDescriptionItem = ( description: , }, ]; + } else if (field === 'references') { + const urls: string[] = get(field, value); + return buildUrlsDescription(label, urls); } else if (Array.isArray(get(field, value))) { const values: string[] = get(field, value); - if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { - return [ - { - title: label, - description: ( - - {values.map((val: string) => - isEmpty(val) ? null : ( - - {val} - - ) - )} - - ), - }, - ]; - } - return []; + return buildStringArrayDescription(label, field, values); + } else if (field === 'severity') { + const val: string = get(field, value); + return buildSeverityDescription(label, val); + } else if (field === 'timeline') { + const timeline = get(field, value) as FieldValueTimeline; + return [ + { + title: label, + description: timeline.title ?? DEFAULT_TIMELINE_TITLE, + }, + ]; + } else if (field === 'riskScore') { + const description: string = get(field, value); + return [ + { + title: label, + description, + }, + ]; } const description: string = get(field, value); if (!isEmpty(description)) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx index 0995e0e916652..9695fd21067ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx @@ -11,7 +11,7 @@ export const FILTERS_LABEL = i18n.translate('xpack.siem.detectionEngine.createRu }); export const QUERY_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.QueryLabel', { - defaultMessage: 'Query', + defaultMessage: 'Custom query', }); export const SAVED_ID_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.savedIdLabel', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts new file mode 100644 index 0000000000000..d32fbcd725d12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ReactNode } from 'react'; + +import { + IIndexPattern, + esFilters, + FilterManager, + Query, +} from '../../../../../../../../../../src/plugins/data/public'; +import { IMitreEnterpriseAttack } from '../../types'; + +export interface ListItems { + title: NonNullable; + description: NonNullable; +} + +export interface BuildQueryBarDescription { + field: string; + filters: esFilters.Filter[]; + filterManager: FilterManager; + query: Query; + savedId: string; + indexPatterns?: IIndexPattern; +} + +export interface BuildThreatsDescription { + label: string; + threats: IMitreEnterpriseAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx index 381a3138bf617..e10194853e7f9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { ImportRuleModalComponent } from './index'; jest.mock('../../../../../lib/kibana'); @@ -20,6 +19,6 @@ describe('ImportRuleModal', () => { importComplete={jest.fn()} /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 6d50fb768068f..75be92f2fe846 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -28,8 +28,6 @@ import { fold } from 'fp-ts/lib/Either'; import uuid from 'uuid'; import { duplicateRules, RulesSchema } from '../../../../../containers/detection_engine/rules'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; import { useStateToaster } from '../../../../../components/toasters'; import { ndjsonToJSON } from '../json_downloader'; import * as i18n from './translations'; @@ -54,7 +52,6 @@ export const ImportRuleModalComponent = ({ }: ImportRuleModalProps) => { const [selectedFiles, setSelectedFiles] = useState(null); const [isImporting, setIsImporting] = useState(false); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const [, dispatchToaster] = useStateToaster(); const cleanupAndCloseModal = () => { @@ -89,7 +86,7 @@ export const ImportRuleModalComponent = ({ }, identity) ); - const duplicatedRules = await duplicateRules({ rules: decodedRules, kbnVersion }); + const duplicatedRules = await duplicateRules({ rules: decodedRules }); importComplete(); cleanupAndCloseModal(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx index d7a508e2c53e3..859918cdc8e60 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx @@ -5,8 +5,7 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; +import React from 'react'; import { JSONDownloaderComponent, jsonToNDJSON, ndjsonToJSON } from './index'; const jsonArray = [ @@ -37,7 +36,7 @@ describe('JSONDownloader', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); describe('jsonToNDJSON', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts new file mode 100644 index 0000000000000..1202fe54ad194 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isEmpty } from 'lodash/fp'; + +import { IMitreAttack } from '../../types'; + +export const isMitreAttackInvalid = ( + tacticName: string | null | undefined, + techniques: IMitreAttack[] | null | undefined +) => { + if (isEmpty(tacticName) || (tacticName !== 'none' && isEmpty(techniques))) { + return true; + } + return false; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index a777506ee12ae..2c19e99e90114 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -8,27 +8,30 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, - EuiSelect, + EuiSuperSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiComboBox, - EuiFormControlLayout, + EuiText, } from '@elastic/eui'; import { isEmpty, kebabCase, camelCase } from 'lodash/fp'; -import React, { ChangeEvent, useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; -import * as RuleI18n from '../../translations'; +import * as Rulei18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; -import * as I18n from './translations'; +import { threatsDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; +import { isMitreAttackInvalid } from './helpers'; +import * as i18n from './translations'; -const MyEuiFormControlLayout = styled(EuiFormControlLayout)` - &.euiFormControlLayout--compressed { - height: fit-content !important; - } +const MitreContainer = styled.div` + margin-top: 16px; +`; +const MyEuiSuperSelect = styled(EuiSuperSelect)` + width: 280px; `; interface AddItemProps { field: FieldHook; @@ -38,12 +41,18 @@ interface AddItemProps { } export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { + const [showValidation, setShowValidation] = useState(false); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const removeItem = useCallback( (index: number) => { const values = field.value as string[]; - field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; + if (isEmpty(newValues)) { + field.setValue(threatsDefault); + } else { + field.setValue(newValues); + } }, [field] ); @@ -61,9 +70,9 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI }, [field]); const updateTactic = useCallback( - (index: number, event: ChangeEvent) => { + (index: number, value: string) => { const values = field.value as IMitreEnterpriseAttack[]; - const { id, reference, name } = tacticsOptions.find(t => t.value === event.target.value) || { + const { id, reference, name } = tacticsOptions.find(t => t.value === value) || { id: '', name: '', reference: '', @@ -97,75 +106,105 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI const values = field.value as IMitreEnterpriseAttack[]; + const getSelectTactic = (tacticName: string, index: number, disabled: boolean) => ( + {i18n.TACTIC_PLACEHOLDER}, + value: 'none', + disabled, + }, + ] + : []), + ...tacticsOptions.map(t => ({ + inputDisplay: <>{t.text}, + value: t.value, + disabled, + })), + ]} + aria-label="" + onChange={updateTactic.bind(null, index)} + fullWidth={false} + valueOfSelected={camelCase(tacticName)} + /> + ); + + const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => { + const invalid = isMitreAttackInvalid(item.tactic.name, item.techniques); + return ( + + + t.tactics.includes(kebabCase(item.tactic.name)))} + selectedOptions={item.techniques} + onChange={updateTechniques.bind(null, index)} + isDisabled={disabled || item.tactic.name === 'none'} + fullWidth={true} + isInvalid={showValidation && invalid} + onBlur={() => setShowValidation(true)} + /> + {showValidation && invalid && ( + +

{errorMessage}

+
+ )} +
+ + removeItem(index)} + aria-label={Rulei18n.DELETE} + /> + +
+ ); + }; + return ( - - <> - {values.map((item, index) => { - const euiSelectFieldProps = { - disabled: isDisabled, - }; - return ( -
- - - ({ text: t.text, value: t.value })), - ]} - aria-label="" - onChange={updateTactic.bind(null, index)} - prepend={I18n.TACTIC} - compressed - fullWidth={false} - value={camelCase(item.tactic.name)} - {...euiSelectFieldProps} - /> - - - - - t.tactics.includes(kebabCase(item.tactic.name)) - )} - selectedOptions={item.techniques} - onChange={updateTechniques.bind(null, index)} - isDisabled={isDisabled} - fullWidth={true} - /> - - - - removeItem(index)} - aria-label={RuleI18n.DELETE} - /> - - - {values.length - 1 !== index && } -
- ); - })} - - {I18n.ADD_MITRE_ATTACK} - - -
+ + {values.map((item, index) => ( +
+ + + {index === 0 ? ( + + <>{getSelectTactic(item.tactic.name, index, isDisabled)} + + ) : ( + getSelectTactic(item.tactic.name, index, isDisabled) + )} + + + {index === 0 ? ( + + <>{getSelectTechniques(item, index, isDisabled)} + + ) : ( + getSelectTechniques(item, index, isDisabled) + )} + + + {values.length - 1 !== index && } +
+ ))} + + {i18n.ADD_MITRE_ATTACK} + +
); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts index 22ee6cc3ef911..557e91691b6c7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts @@ -7,18 +7,18 @@ import { i18n } from '@kbn/i18n'; export const TACTIC = i18n.translate('xpack.siem.detectionEngine.mitreAttack.tacticsDescription', { - defaultMessage: 'Tactic', + defaultMessage: 'tactic', }); -export const TECHNIQUES = i18n.translate( +export const TECHNIQUE = i18n.translate( 'xpack.siem.detectionEngine.mitreAttack.techniquesDescription', { - defaultMessage: 'Techniques', + defaultMessage: 'technique', } ); export const ADD_MITRE_ATTACK = i18n.translate('xpack.siem.detectionEngine.mitreAttack.addTitle', { - defaultMessage: 'Add MITRE ATT&CK threat', + defaultMessage: 'Add MITRE ATT&CK\\u2122 threat', }); export const TECHNIQUES_PLACEHOLDER = i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx new file mode 100644 index 0000000000000..873e0c2184c61 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.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 { EuiFormRow } from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; + +export interface FieldValueTimeline { + id: string | null; + title: string | null; +} + +interface QueryBarDefineRuleProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; +} + +export const PickTimeline = ({ + dataTestSubj, + field, + idAria, + isDisabled = false, +}: QueryBarDefineRuleProps) => { + const [timelineId, setTimelineId] = useState(null); + const [timelineTitle, setTimelineTitle] = useState(null); + + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + useEffect(() => { + const { id, title } = field.value as FieldValueTimeline; + if (timelineTitle !== title && timelineId !== id) { + setTimelineId(id); + setTimelineTitle(title); + } + }, [field.value]); + + const handleOnTimelineChange = useCallback( + (title: string, id: string | null) => { + if (id === null) { + field.setValue({ id, title: null }); + } else if (timelineTitle !== title && timelineId !== id) { + field.setValue({ id, title }); + } + }, + [field] + ); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index c294ec24c4cb7..3e39beb6e61b7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -6,7 +6,7 @@ import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; @@ -19,11 +19,18 @@ import { SavedQueryTimeFilter, } from '../../../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../../../containers/source'; +import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; +import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; import { QueryBar } from '../../../../../components/query_bar'; +import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; +import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; import { useKibana } from '../../../../../lib/kibana'; +import { TimelineModel } from '../../../../../store/timeline/model'; import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; - import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import * as i18n from './translations'; export interface FieldValueQueryBar { filters: esFilters.Filter[]; @@ -31,11 +38,14 @@ export interface FieldValueQueryBar { saved_id: string | null; } interface QueryBarDefineRuleProps { + browserFields: BrowserFields; dataTestSubj: string; field: FieldHook; idAria: string; isLoading: boolean; indexPattern: IIndexPattern; + onCloseTimelineSearch: () => void; + openTimelineSearch: boolean; resizeParentContainer?: (height: number) => void; } @@ -56,14 +66,18 @@ const StyledEuiFormRow = styled(EuiFormRow)` // TODO need to add disabled in the SearchBar export const QueryBarDefineRule = ({ + browserFields, dataTestSubj, field, idAria, indexPattern, isLoading = false, + onCloseTimelineSearch, + openTimelineSearch = false, resizeParentContainer, }: QueryBarDefineRuleProps) => { const [originalHeight, setOriginalHeight] = useState(-1); + const [loadingTimeline, setLoadingTimeline] = useState(false); const [savedQuery, setSavedQuery] = useState(null); const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); @@ -168,6 +182,38 @@ export const QueryBarDefineRule = ({ [field.value] ); + const onCloseTimelineModal = useCallback(() => { + setLoadingTimeline(true); + onCloseTimelineSearch(); + }, [onCloseTimelineSearch]); + + const onOpenTimeline = useCallback( + (timeline: TimelineModel) => { + setLoadingTimeline(false); + const newQuery = { + query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', + language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', + }; + const dataProvidersDsl = + timeline.dataProviders != null && timeline.dataProviders.length > 0 + ? convertKueryToElasticSearchQuery( + buildGlobalQuery(timeline.dataProviders, browserFields), + indexPattern + ) + : ''; + const newFilters = timeline.filters ?? []; + field.setValue({ + filters: + dataProvidersDsl !== '' + ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] + : newFilters, + query: newQuery, + saved_id: '', + }); + }, + [browserFields, field, indexPattern] + ); + const onMutation = (event: unknown, observer: unknown) => { if (resizeParentContainer != null) { const suggestionContainer = document.getElementById('kbnTypeahead__items'); @@ -189,39 +235,51 @@ export const QueryBarDefineRule = ({ } }; + const actionTimelineToHide = useMemo(() => ['duplicate'], []); + return ( - - + - {mutationRef => ( -
- -
- )} -
-
+ + {mutationRef => ( +
+ +
+ )} +
+ + {openTimelineSearch ? ( + + ) : null} + ); }; diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx similarity index 50% rename from x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx index 3edb289985d55..9b14e4f8599da 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as logPositionActions from './actions'; -import * as logPositionSelectors from './selectors'; +import { i18n } from '@kbn/i18n'; -export { logPositionActions, logPositionSelectors }; -export * from './reducer'; +export const IMPORT_TIMELINE_MODAL = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle', + { + defaultMessage: 'Import query from saved timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx new file mode 100644 index 0000000000000..6ec76bacc2323 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.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. + */ + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const ReadOnlyCallOutComponent = () => { + const [showCallOut, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCallOut ? ( + +

{i18n.READ_ONLY_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const ReadOnlyCallOut = memo(ReadOnlyCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts new file mode 100644 index 0000000000000..c3429f4365031 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts @@ -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. + */ + +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.readOnlyCallOutTitle', + { + defaultMessage: 'Rule permissions required', + } +); + +export const READ_ONLY_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.readOnlyCallOutMsg', + { + defaultMessage: + 'You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance.', + } +); + +export const DISMISS_CALLOUT = i18n.translate('xpack.siem.detectionEngine.dismissButton', { + defaultMessage: 'Dismiss', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap index f264dde07c594..604f86866d565 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap @@ -11,7 +11,6 @@ exports[`RuleSwitch renders correctly against snapshot 1`] = ` { test('renders correctly against snapshot', () => { const wrapper = shallow( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index 54f0fc453830e..09be3df7d6929 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -15,9 +15,7 @@ import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; import React, { useCallback, useState, useEffect } from 'react'; -import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; import { enableRules } from '../../../../../containers/detection_engine/rules'; -import { useUiSetting$ } from '../../../../../lib/kibana'; import { enableRulesAction } from '../../all/actions'; import { Action } from '../../all/reducer'; @@ -34,6 +32,7 @@ export interface RuleSwitchProps { dispatch?: React.Dispatch; id: string; enabled: boolean; + isDisabled?: boolean; isLoading?: boolean; optionLabel?: string; } @@ -44,25 +43,24 @@ export interface RuleSwitchProps { export const RuleSwitchComponent = ({ dispatch, id, + isDisabled, isLoading, enabled, optionLabel, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); const [myEnabled, setMyEnabled] = useState(enabled ?? false); - const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const onRuleStateChange = useCallback( async (event: EuiSwitchEvent) => { setMyIsLoading(true); if (dispatch != null) { - await enableRulesAction([id], event.target.checked!, dispatch, kbnVersion); + await enableRulesAction([id], event.target.checked!, dispatch); } else { try { const updatedRules = await enableRules({ ids: [id], enabled: event.target.checked!, - kbnVersion, }); setMyEnabled(updatedRules[0].enabled); } catch { @@ -71,7 +69,7 @@ export const RuleSwitchComponent = ({ } setMyIsLoading(false); }, - [dispatch, id, kbnVersion] + [dispatch, id] ); useEffect(() => { @@ -96,7 +94,7 @@ export const RuleSwitchComponent = ({ data-test-subj="rule-switch" label={optionLabel ?? ''} showLabel={!isEmpty(optionLabel)} - disabled={false} + disabled={isDisabled} checked={myEnabled} onChange={onRuleStateChange} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 2e57ff8ba2c4f..8097c27cddfe8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; @@ -32,6 +32,10 @@ const StyledEuiFormRow = styled(EuiFormRow)` } `; +const MyEuiSelect = styled(EuiSelect)` + width: auto; +`; + export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: ScheduleItemProps) => { const [timeType, setTimeType] = useState('s'); const [timeVal, setTimeVal] = useState(0); @@ -79,22 +83,33 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu // EUI missing some props const rest = { disabled: isDisabled }; + const label = useMemo( + () => ( + + + {field.label} + + + {field.labelAppend} + + + ), + [field.label, field.labelAppend] + ); return ( } - compressed fullWidth min={0} onChange={onChangeTimeVal} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx index 22b116557ae6e..3ec5bf1a12eb0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx @@ -25,13 +25,15 @@ const RuleStatusIconStyled = styled.div` } `; -export const RuleStatusIcon = memo(({ name, type }) => { +const RuleStatusIconComponent: React.FC = ({ name, type }) => { const theme = useEuiTheme(); - const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorDarkestShade; + const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorPrimary; return ( {type === 'valid' ? : null} ); -}); +}; + +export const RuleStatusIcon = memo(RuleStatusIconComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts deleted file mode 100644 index 7d6e434bcc8c6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as I18n from './translations'; - -export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; - -interface SeverityOptionItem { - value: SeverityValue; - text: string; -} - -export const severityOptions: SeverityOptionItem[] = [ - { value: 'low', text: I18n.LOW }, - { value: 'medium', text: I18n.MEDIUM }, - { value: 'high', text: I18n.HIGH }, - { value: 'critical', text: I18n.CRITICAL }, -]; - -export const defaultRiskScoreBySeverity: Record = { - low: 21, - medium: 47, - high: 73, - critical: 99, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx new file mode 100644 index 0000000000000..9fb64189ebd1a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.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 { EuiHealth } from '@elastic/eui'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import * as I18n from './translations'; + +export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; + +interface SeverityOptionItem { + value: SeverityValue; + inputDisplay: React.ReactElement; +} + +export const severityOptions: SeverityOptionItem[] = [ + { + value: 'low', + inputDisplay: {I18n.LOW}, + }, + { + value: 'medium', + inputDisplay: {I18n.MEDIUM} , + }, + { + value: 'high', + inputDisplay: {I18n.HIGH} , + }, + { + value: 'critical', + inputDisplay: {I18n.CRITICAL} , + }, +]; + +export const defaultRiskScoreBySeverity: Record = { + low: 21, + medium: 47, + high: 73, + critical: 99, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index c0c5ae77a1960..328c4a0f96066 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -6,6 +6,14 @@ import { AboutStepRule } from '../../types'; +export const threatsDefault = [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + techniques: [], + }, +]; + export const stepAboutDefaultValue: AboutStepRule = { name: '', description: '', @@ -15,11 +23,9 @@ export const stepAboutDefaultValue: AboutStepRule = { references: [''], falsePositives: [''], tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - techniques: [], - }, - ], + timeline: { + id: null, + title: null, + }, + threats: threatsDefault, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts new file mode 100644 index 0000000000000..99b01c8b22974 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; + +const urlExpression = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi; + +export const isUrlInvalid = (url: string | null | undefined) => { + if (!isEmpty(url) && url != null && url.match(urlExpression) == null) { + return true; + } + return false; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index e266c0b9ab47d..8956776dcd3b2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -7,17 +7,21 @@ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEqual, get } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; import * as RuleI18n from '../../translations'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { AddItem } from '../add_item_form'; +import { StepRuleDescription } from '../description_step'; +import { AddMitreThreat } from '../mitre'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; + import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; import { stepAboutDefaultValue } from './default_value'; +import { isUrlInvalid } from './helpers'; import { schema } from './schema'; import * as I18n from './translations'; -import { StepRuleDescription } from '../description_step'; -import { AddMitreThreat } from '../mitre'; +import { PickTimeline } from '../pick_timeline'; const CommonUseField = getUseField({ component: Field }); @@ -25,6 +29,10 @@ interface StepAboutRuleProps extends RuleStepProps { defaultValues?: AboutStepRule | null; } +const TagContainer = styled.div` + margin-top: 16px; +`; + export const StepAboutRule = memo( ({ defaultValues, @@ -90,7 +98,6 @@ export const StepAboutRule = memo( idAria: 'detectionEngineStepAboutRuleName', 'data-test-subj': 'detectionEngineStepAboutRuleName', euiFieldProps: { - compressed: true, fullWidth: false, disabled: isLoading, }, @@ -99,11 +106,9 @@ export const StepAboutRule = memo( ( idAria: 'detectionEngineStepAboutRuleSeverity', 'data-test-subj': 'detectionEngineStepAboutRuleSeverity', euiFieldProps: { - compressed: true, fullWidth: false, disabled: isLoading, options: severityOptions, @@ -129,29 +133,38 @@ export const StepAboutRule = memo( euiFieldProps: { max: 100, min: 0, - compressed: true, fullWidth: false, disabled: isLoading, options: severityOptions, + showTicks: true, + tickInterval: 25, }, }} /> + ( path="threats" component={AddMitreThreat} componentProps={{ - compressed: true, idAria: 'detectionEngineStepAboutRuleMitreThreats', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepAboutRuleMitreThreats', }} /> - + + + {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; @@ -202,7 +216,7 @@ export const StepAboutRule = memo( > - {myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE} + {RuleI18n.CONTINUE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index c72312bb90836..008a1b48610d6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash/fp'; import React from 'react'; import * as RuleI18n from '../../translations'; @@ -18,6 +17,8 @@ import { ValidationFunc, ERROR_CODE, } from '../shared_imports'; +import { isMitreAttackInvalid } from '../mitre/helpers'; +import { isUrlInvalid } from './helpers'; import * as I18n from './translations'; const { emptyField } = fieldValidators; @@ -63,7 +64,7 @@ export const schema: FormSchema = { ], }, severity: { - type: FIELD_TYPES.SELECT, + type: FIELD_TYPES.SUPER_SELECT, label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', { @@ -92,6 +93,14 @@ export const schema: FormSchema = { } ), }, + timeline: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } + ), + }, references: { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', @@ -100,12 +109,34 @@ export const schema: FormSchema = { } ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as string[]).forEach(url => { + if (isUrlInvalid(url)) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_FORMAT', + path, + message: I18n.URL_FORMAT_INVALID, + } + : undefined; + }, + }, + ], }, falsePositives: { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', { - defaultMessage: 'False positives', + defaultMessage: 'False positives examples', } ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, @@ -114,7 +145,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', { - defaultMessage: 'MITRE ATT&CK', + defaultMessage: 'MITRE ATT&CK\\u2122', } ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, @@ -126,7 +157,7 @@ export const schema: FormSchema = { const [{ value, path }] = args; let hasError = false; (value as IMitreEnterpriseAttack[]).forEach(v => { - if (isEmpty(v.tactic.name) || (v.tactic.name !== 'none' && isEmpty(v.techniques))) { + if (isMitreAttackInvalid(v.tactic.name, v.techniques)) { hasError = true; } }); @@ -146,6 +177,13 @@ export const schema: FormSchema = { label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { defaultMessage: 'Tags', }), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText', + { + defaultMessage: + 'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.', + } + ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts index 017d4fe6fdf49..9323769765739 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts @@ -9,14 +9,14 @@ import { i18n } from '@kbn/i18n'; export const ADD_REFERENCE = i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription', { - defaultMessage: 'Add reference', + defaultMessage: 'Add reference URL', } ); export const ADD_FALSE_POSITIVE = i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription', { - defaultMessage: 'Add false positive', + defaultMessage: 'Add false positive example', } ); @@ -54,3 +54,10 @@ export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate( defaultMessage: 'At least one Technique is required with a Tactic.', } ); + +export const URL_FORMAT_INVALID = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError', + { + defaultMessage: 'Url is invalid format', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index cc4e959cc9c78..ecd2ce442238f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { isEqual, get } from 'lodash/fp'; +import { + EuiButtonEmpty, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { isEmpty, isEqual, get } from 'lodash/fp'; import React, { memo, useCallback, useState, useEffect } from 'react'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; @@ -18,7 +24,7 @@ import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { schema } from './schema'; -import * as I18n from './translations'; +import * as i18n from './translations'; const CommonUseField = getUseField({ component: Field }); @@ -34,7 +40,6 @@ const stepDefineDefaultValue = { filters: [], saved_id: null, }, - useIndicesConfig: 'true', }; const getStepDefaultValue = ( @@ -45,7 +50,6 @@ const getStepDefaultValue = ( return { ...defaultValues, isNew: false, - useIndicesConfig: `${isEqual(defaultValues.index, indicesConfig)}`, }; } else { return { @@ -66,13 +70,22 @@ export const StepDefineRule = memo( setForm, setStepData, }) => { - const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); + const [openTimelineSearch, setOpenTimelineSearch] = useState(false); + const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); + const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState( + defaultValues != null ? defaultValues.index : indicesConfig ?? [] + ); const [ - { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - setIndices, - ] = useFetchIndexPatterns(defaultValues != null ? defaultValues.index : indicesConfig ?? []); - const [myStepData, setMyStepData] = useState(stepDefineDefaultValue); + { + browserFields, + indexPatterns: indexPatternQueryBar, + isLoading: indexPatternLoadingQueryBar, + }, + ] = useFetchIndexPatterns(mylocalIndicesConfig); + const [myStepData, setMyStepData] = useState( + getStepDefaultValue(indicesConfig, null) + ); const { form } = useForm({ defaultValue: myStepData, @@ -96,7 +109,7 @@ export const StepDefineRule = memo( const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); if (!isEqual(myDefaultValues, myStepData)) { setMyStepData(myDefaultValues); - setLocalUseIndicesConfig(myDefaultValues.useIndicesConfig); + setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); if (!isReadOnlyView) { Object.keys(schema).forEach(key => { const val = get(key, myDefaultValues); @@ -115,6 +128,19 @@ export const StepDefineRule = memo( } }, [form]); + const handleResetIndices = useCallback(() => { + const indexField = form.getFields().index; + indexField.setValue(indicesConfig); + }, [form, indicesConfig]); + + const handleOpenTimelineSearch = useCallback(() => { + setOpenTimelineSearch(true); + }, []); + + const handleCloseTimelineSearch = useCallback(() => { + setOpenTimelineSearch(false); + }, []); + return isReadOnlyView && myStepData != null ? ( ( ) : ( <>
- + {i18n.RESET_DEFAULT_INDEX} + + ) : null, + }} componentProps={{ idAria: 'detectionEngineStepDefineRuleIndices', 'data-test-subj': 'detectionEngineStepDefineRuleIndices', euiFieldProps: { - compressed: true, fullWidth: true, isDisabled: isLoading, + placeholder: '', }, }} /> + {i18n.IMPORT_TIMELINE_QUERY} + + ), + }} component={QueryBarDefineRule} componentProps={{ - compressed: true, + browserFields, loading: indexPatternLoadingQueryBar, idAria: 'detectionEngineStepDefineRuleQueryBar', indexPattern: indexPatternQueryBar, isDisabled: isLoading, isLoading: indexPatternLoadingQueryBar, dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, resizeParentContainer, }} /> - - {({ useIndicesConfig }) => { - if (localUseIndicesConfig !== useIndicesConfig) { - const indexField = form.getFields().index; - if ( - indexField != null && - useIndicesConfig === 'true' && - !isEqual(indexField.value, indicesConfig) - ) { - indexField.setValue(indicesConfig); - setIndices(indicesConfig); - } else if ( - indexField != null && - useIndicesConfig === 'false' && - isEqual(indexField.value, indicesConfig) - ) { - indexField.setValue([]); - setIndices([]); + + {({ index }) => { + if (index != null) { + if (isEqual(index, indicesConfig) && !localUseIndicesConfig) { + setLocalUseIndicesConfig(true); + } + if (!isEqual(index, indicesConfig) && localUseIndicesConfig) { + setLocalUseIndicesConfig(false); + } + if (index != null && !isEmpty(index) && !isEqual(index, mylocalIndicesConfig)) { + setMyLocalIndicesConfig(index); } - setLocalUseIndicesConfig(useIndicesConfig); } - return null; }} @@ -208,7 +223,7 @@ export const StepDefineRule = memo( > - {myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE} + {RuleI18n.CONTINUE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 9b54ada8227c6..079ec0dab4c5a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -10,7 +10,6 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; -import * as RuleI18n from '../../translations'; import { FieldValueQueryBar } from '../query_bar'; import { ERROR_CODE, @@ -19,33 +18,27 @@ import { FormSchema, ValidationFunc, } from '../shared_imports'; -import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; const { emptyField } = fieldValidators; export const schema: FormSchema = { - useIndicesConfig: { - type: FIELD_TYPES.RADIO_GROUP, + index: { + type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldIndicesTypeLabel', + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', { - defaultMessage: 'Indices type', + defaultMessage: 'Index patterns', } ), - }, - index: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', { - defaultMessage: 'Indices', - }), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + helpText: {INDEX_HELPER_TEXT}, validations: [ { validator: emptyField( i18n.translate( 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', { - defaultMessage: 'An output indice name for signals is required.', + defaultMessage: 'A minimum of one index pattern is required.', } ) ), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx index 0050c59a4a2c8..8394f090e346c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx @@ -33,3 +33,25 @@ export const CUSTOM_INDICES = i18n.translate( defaultMessage: 'Provide custom list of indices', } ); + +export const INDEX_HELPER_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesHelperDescription', + { + defaultMessage: + 'Enter the pattern of Elasticsearch indices where you would like this rule to run. By default, these will include index patterns defined in SIEM advanced settings.', + } +); + +export const RESET_DEFAULT_INDEX = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.resetDefaultIndicesButton', + { + defaultMessage: 'Reset to default index patterns', + } +); + +export const IMPORT_TIMELINE_QUERY = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton', + { + defaultMessage: 'Import query from saved timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx index 21b38a83dad9d..88cecadb8b137 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx @@ -17,15 +17,15 @@ interface StepPanelProps { } const MyPanel = styled(EuiPanel)` - poistion: relative; + position: relative; `; -export const StepPanel = memo(({ children, loading, title }) => { - return ( - - {loading && } - - {children} - - ); -}); +const StepPanelComponent: React.FC = ({ children, loading, title }) => ( + + {loading && } + + {children} + +); + +export const StepPanel = memo(StepPanelComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 6f7e49bc8ab9a..35b8ca6650bf6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -92,7 +92,6 @@ export const StepScheduleRule = memo( path="interval" component={ScheduleItem} componentProps={{ - compressed: true, idAria: 'detectionEngineStepScheduleRuleInterval', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepScheduleRuleInterval', @@ -102,7 +101,6 @@ export const StepScheduleRule = memo( path="from" component={ScheduleItem} componentProps={{ - compressed: true, idAria: 'detectionEngineStepScheduleRuleFrom', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepScheduleRuleFrom', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index a25ccce569dd4..ce91e15cdcf0d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -40,14 +40,14 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => { }; const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const { queryBar, useIndicesConfig, isNew, ...rest } = defineStepData; + const { queryBar, isNew, ...rest } = defineStepData; const { filters, query, saved_id: savedId } = queryBar; return { ...rest, language: query.language, filters, query: query.query as string, - ...(savedId != null ? { saved_id: savedId } : {}), + ...(savedId != null && savedId !== '' ? { saved_id: savedId } : {}), }; }; @@ -72,11 +72,25 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul }; const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threats, isNew, ...rest } = aboutStepData; + const { + falsePositives, + references, + riskScore, + threats, + timeline, + isNew, + ...rest + } = aboutStepData; return { false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), risk_score: riskScore, + ...(timeline.id != null && timeline.title != null + ? { + timeline_id: timeline.id, + timeline_title: timeline.title, + } + : {}), threats: threats .filter(threat => threat.tactic.name !== 'none') .map(threat => ({ @@ -97,7 +111,7 @@ export const formatRule = ( scheduleData: ScheduleStepRule, ruleId?: string ): NewRule => { - const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query'; + const type: FormatRuleType = !isEmpty(defineStepData.queryBar.saved_id) ? 'saved_query' : 'query'; const persistData = { type, ...formatDefineStepData(defineStepData), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 3e8dbeba89546..9a0f41bbd8c51 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -12,12 +12,14 @@ import styled from 'styled-components'; import { HeaderPage } from '../../../../components/header_page'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { WrapperPage } from '../../../../components/wrapper_page'; +import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; +import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; +import { FormData, FormHook } from '../components/shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; -import { usePersistRule } from '../../../../containers/detection_engine/rules'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; import * as RuleI18n from '../translations'; import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types'; import { formatRule } from './helpers'; @@ -28,17 +30,50 @@ const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.schedu const ResizeEuiPanel = styled(EuiPanel)<{ height?: number; }>` + .euiAccordion__iconWrapper { + display: none; + } .euiAccordion__childWrapper { height: ${props => (props.height !== -1 ? `${props.height}px !important` : 'auto')}; } + .euiAccordion__button { + cursor: default !important; + &:hover { + text-decoration: none !important; + } + } +`; + +const MyEuiPanel = styled(EuiPanel)` + .euiAccordion__iconWrapper { + display: none; + } + .euiAccordion__button { + cursor: default !important; + &:hover { + text-decoration: none !important; + } + } `; export const CreateRuleComponent = React.memo(() => { + const { + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasManageApiKey, + } = useUserInfo(); const [heightAccordion, setHeightAccordion] = useState(-1); - const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); + const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); const scheduleRuleRef = useRef(null); + const stepsForm = useRef | null>>({ + [RuleStep.defineRule]: null, + [RuleStep.aboutRule]: null, + [RuleStep.scheduleRule]: null, + }); const stepsData = useRef>({ [RuleStep.defineRule]: { isValid: false, data: {} }, [RuleStep.aboutRule]: { isValid: false, data: {} }, @@ -50,6 +85,18 @@ export const CreateRuleComponent = React.memo(() => { [RuleStep.scheduleRule]: false, }); const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; + + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } else if (userHasNoPermissions) { + return ; + } const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { @@ -57,11 +104,17 @@ export const CreateRuleComponent = React.memo(() => { if (isValid) { const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); if ([0, 1].includes(stepRuleIdx)) { - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [step]: true, - }); - if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + [stepsRuleOrder[stepRuleIdx + 1]]: false, + }); + } else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + }); openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); } @@ -80,9 +133,13 @@ export const CreateRuleComponent = React.memo(() => { } } }, - [openAccordionId, stepsData.current, setRule] + [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] ); + const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { + stepsForm.current[step] = form; + }, []); + const getAccordionType = useCallback( (accordionId: RuleStep) => { if (accordionId === openAccordionId) { @@ -135,42 +192,38 @@ export const CreateRuleComponent = React.memo(() => { (id: RuleStep, isOpen: boolean) => { const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId); const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); - const isLatestStepsRuleValid = - stepRuleIdx === 0 - ? true - : stepsRuleOrder - .filter((stepRule, index) => index < stepRuleIdx) - .every(stepRule => stepsData.current[stepRule].isValid); - if (stepRuleIdx < activeRuleIdx && !isOpen) { + if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) { openCloseAccordion(id); } else if (stepRuleIdx >= activeRuleIdx) { if ( - openAccordionId != null && openAccordionId !== id && !stepsData.current[openAccordionId].isValid && !isStepRuleInReadOnlyView[id] && isOpen ) { openCloseAccordion(id); - } else if (!isLatestStepsRuleValid && isOpen) { - openCloseAccordion(id); - } else if (id !== openAccordionId && isOpen) { - setOpenAccordionId(id); } } }, - [isStepRuleInReadOnlyView, openAccordionId] + [isStepRuleInReadOnlyView, openAccordionId, stepsData] ); const manageIsEditable = useCallback( - (id: RuleStep) => { - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [id]: false, - }); + async (id: RuleStep) => { + const activeForm = await stepsForm.current[openAccordionId]?.submit(); + if (activeForm != null && activeForm?.isValid) { + setOpenAccordionId(id); + openCloseAccordion(openAccordionId); + + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true, + [id]: false, + }); + } }, - [isStepRuleInReadOnlyView] + [isStepRuleInReadOnlyView, openAccordionId] ); if (isSaved) { @@ -183,7 +236,7 @@ export const CreateRuleComponent = React.memo(() => { @@ -201,7 +254,7 @@ export const CreateRuleComponent = React.memo(() => { size="xs" onClick={manageIsEditable.bind(null, RuleStep.defineRule)} > - {`Edit`} + {i18n.EDIT_RULE} ) } @@ -209,14 +262,15 @@ export const CreateRuleComponent = React.memo(() => { setHeightAccordion(height)} /> - + { size="xs" onClick={manageIsEditable.bind(null, RuleStep.aboutRule)} > - {`Edit`} + {i18n.EDIT_RULE} ) } @@ -239,13 +293,14 @@ export const CreateRuleComponent = React.memo(() => { - + - + { size="xs" onClick={manageIsEditable.bind(null, RuleStep.scheduleRule)} > - {`Edit`} + {i18n.EDIT_RULE} ) } @@ -268,11 +323,12 @@ export const CreateRuleComponent = React.memo(() => { - + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts index 884f3f3741228..329bcc286fb70 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts @@ -9,3 +9,7 @@ import { i18n } from '@kbn/i18n'; export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', { defaultMessage: 'Create new rule', }); + +export const EDIT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.editRuleButton', { + defaultMessage: 'Edit', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 1bc2bc24517e3..679f42f025196 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -6,10 +6,12 @@ import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { memo, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import React, { memo, useCallback, useMemo } from 'react'; +import { Redirect, useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; +import { ActionCreator } from 'typescript-fsa'; +import { connect } from 'react-redux'; import { FiltersGlobal } from '../../../../components/filters_global'; import { FormattedDate } from '../../../../components/formatted_date'; import { HeaderPage } from '../../../../components/header_page'; @@ -24,210 +26,300 @@ import { } from '../../../../containers/source'; import { SpyRoute } from '../../../../utils/route/spy_routes'; -import { SignalsCharts } from '../../components/signals_chart'; +import { SignalsHistogramPanel } from '../../components/signals_histogram_panel'; import { SignalsTable } from '../../components/signals'; +import { useUserInfo } from '../../components/user_info'; import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; import { useSignalInfo } from '../../components/signals_info'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { buildSignalsRuleIdFilter } from '../../components/signals/default_config'; +import { NoWriteSignalsCallOut } from '../../components/no_write_signals_callout'; import * as detectionI18n from '../../translations'; +import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; import { getStepsData } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; +import { signalsHistogramOptions } from '../../components/signals_histogram_panel/config'; +import { InputsModelId } from '../../../../store/inputs/constants'; +import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { Query } from '../../../../../../../../../src/plugins/data/common/query'; +import { inputsSelectors } from '../../../../store/inputs'; +import { State } from '../../../../store'; +import { InputsRange } from '../../../../store/inputs/model'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; -interface RuleDetailsComponentProps { - signalsIndex: string | null; +interface ReduxProps { + filters: esFilters.Filter[]; + query: Query; } -export const RuleDetailsComponent = memo(({ signalsIndex }) => { - const { ruleId } = useParams(); - const [loading, rule] = useRule(ruleId); - const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ - rule, - detailsView: true, - }); - const [lastSignals] = useSignalInfo({ ruleId }); - - const title = loading === true || rule === null ? : rule.name; - const subTitle = useMemo( - () => - loading === true || rule === null ? ( - - ) : ( - [ - - ), - }} - />, - rule?.updated_by != null ? ( +export interface DispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; +} + +type RuleDetailsComponentProps = ReduxProps & DispatchProps; + +const RuleDetailsComponent = memo( + ({ filters, query, setAbsoluteRangeDatePicker }) => { + const { + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasManageApiKey, + hasIndexWrite, + signalIndexName, + } = useUserInfo(); + const { ruleId } = useParams(); + const [isLoading, rule] = useRule(ruleId); + const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ + rule, + detailsView: true, + }); + const [lastSignals] = useSignalInfo({ ruleId }); + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; + + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } + + const title = isLoading === true || rule === null ? : rule.name; + const subTitle = useMemo( + () => + isLoading === true || rule === null ? ( + + ) : ( + [ ), }} - /> - ) : ( - '' - ), - ] - ), - [loading, rule] - ); - - const signalDefaultFilters = useMemo( - () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), - [ruleId] - ); - return ( - <> - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from }) => ( - - - - - - - - {detectionI18n.LAST_SIGNAL} - {': '} - {lastSignals} - - ) : null, - 'Status: Comming Soon', - ]} - title={title} - > - - - - + />, + rule?.updated_by != null ? ( + + ), + }} + /> + ) : ( + '' + ), + ] + ), + [isLoading, rule] + ); - - - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - - - - - - - - - - - {defineRuleData != null && ( - - )} - - - - - - {aboutRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), + [ruleId] + ); + + const signalMergedFilters = useMemo(() => [...signalDefaultFilters, ...filters], [ + signalDefaultFilters, + filters, + ]); + + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + return ( + <> + {hasIndexWrite != null && !hasIndexWrite && } + {userHasNoPermissions && } + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ to, from }) => ( + + + + + + + + {detectionI18n.LAST_SIGNAL} + {': '} + {lastSignals} + + ) : null, + 'Status: Comming Soon', + ]} + title={title} + > + + + - )} - - - + - + + + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + + + + - + - + + + + {defineRuleData != null && ( + + )} + + + + + + {aboutRuleData != null && ( + + )} + + - {ruleId != null && ( - + + {scheduleRuleData != null && ( + + )} + + + + + + - )} - - - )} - - ) : ( - - - - - - ); - }} - - - - - ); -}); + + + + {ruleId != null && ( + + )} + + + )} + + ) : ( + + + + + + ); + }} + + + + + ); + } +); RuleDetailsComponent.displayName = 'RuleDetailsComponent'; + +const makeMapStateToProps = () => { + const getGlobalInputs = inputsSelectors.globalSelector(); + return (state: State) => { + const globalInputs: InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + + return { + query, + filters, + }; + }; +}; + +export const RuleDetails = connect(makeMapStateToProps, { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, +})(RuleDetailsComponent); + +RuleDetails.displayName = 'RuleDetails'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 8e32f82dff0b1..e583461f52439 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -22,6 +22,7 @@ import { WrapperPage } from '../../../../components/wrapper_page'; import { SpyRoute } from '../../../../utils/route/spy_routes'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { useUserInfo } from '../../components/user_info'; import { FormHook, FormData } from '../components/shared_imports'; import { StepPanel } from '../components/step_panel'; import { StepAboutRule } from '../components/step_about_rule'; @@ -47,8 +48,28 @@ interface ScheduleStepRuleForm extends StepRuleForm { } export const EditRuleComponent = memo(() => { + const { + loading: initLoading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasManageApiKey, + } = useUserInfo(); const { ruleId } = useParams(); const [loading, rule] = useRule(ruleId); + + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } else if (userHasNoPermissions) { + return ; + } + const [initForm, setInitForm] = useState(false); const [myAboutRuleForm, setMyAboutRuleForm] = useState({ data: null, @@ -88,7 +109,7 @@ export const EditRuleComponent = memo(() => { content: ( <> - + {myDefineRuleForm.data != null && ( { content: ( <> - + {myAboutRuleForm.data != null && ( { content: ( <> - + {myScheduleRuleForm.data != null && ( { ], [ loading, + initLoading, isLoading, myAboutRuleForm, myDefineRuleForm, @@ -249,7 +271,7 @@ export const EditRuleComponent = memo(() => { }, []); if (isSaved || (rule != null && rule.immutable)) { - return ; + return ; } return ( @@ -257,7 +279,7 @@ export const EditRuleComponent = memo(() => { { responsive={false} > - + {i18n.CANCEL} - + {i18n.SAVE_CHANGES} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 46301ae808919..cc0882dd7e426 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -5,6 +5,7 @@ */ import { pick } from 'lodash/fp'; +import { useLocation } from 'react-router-dom'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; @@ -33,7 +34,6 @@ export const getStepsData = ({ filters: rule.filters as esFilters.Filter[], saved_id: rule.saved_id ?? null, }, - useIndicesConfig: 'true', } : null; const aboutRuleData: AboutStepRule | null = @@ -45,6 +45,10 @@ export const getStepsData = ({ threats: rule.threats as IMitreEnterpriseAttack[], falsePositives: rule.false_positives, riskScore: rule.risk_score, + timeline: { + id: rule.timeline_id ?? null, + title: rule.timeline_title ?? null, + }, } : null; const scheduleRuleData: ScheduleStepRule | null = @@ -61,3 +65,5 @@ export const getStepsData = ({ return { aboutRuleData, defineRuleData, scheduleRuleData }; }; + +export const useQuery = () => new URLSearchParams(useLocation().search); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 8b4cc2a213589..dd46b33ca7257 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -5,9 +5,11 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { Redirect } from 'react-router-dom'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; @@ -16,15 +18,34 @@ import { SpyRoute } from '../../../utils/route/spy_routes'; import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; +import { ReadOnlyCallOut } from './components/read_only_callout'; +import { useUserInfo } from '../components/user_info'; import * as i18n from './translations'; export const RulesComponent = React.memo(() => { const [showImportModal, setShowImportModal] = useState(false); const [importCompleteToggle, setImportCompleteToggle] = useState(false); + const { + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasManageApiKey, + } = useUserInfo(); + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; const lastCompletedRun = undefined; return ( <> + {userHasNoPermissions && } setShowImportModal(false)} @@ -32,7 +53,10 @@ export const RulesComponent = React.memo(() => { /> { { setShowImportModal(true); }} @@ -59,16 +84,23 @@ export const RulesComponent = React.memo(() => { {i18n.IMPORT_RULE} - - + {i18n.ADD_NEW_RULE} - - + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index ecd6bef942bfb..8d4407b9f73e8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -207,15 +207,15 @@ export const COLUMN_ACTIVATE = i18n.translate( ); export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.defineRuleTitle', { - defaultMessage: 'Define Rule', + defaultMessage: 'Define rule', }); export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.aboutRuleTitle', { - defaultMessage: 'About Rule', + defaultMessage: 'About rule', }); export const SCHEDULE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.scheduleRuleTitle', { - defaultMessage: 'Schedule Rule', + defaultMessage: 'Schedule rule', }); export const DEFINITION = i18n.translate('xpack.siem.detectionEngine.rules.stepDefinitionTitle', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 9b535034810bd..541b058951be7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -8,6 +8,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/common'; import { Rule } from '../../../containers/detection_engine/rules'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from './components/shared_imports'; +import { FieldValueTimeline } from './components/pick_timeline'; export interface EuiBasicTableSortTypes { field: string; @@ -76,11 +77,11 @@ export interface AboutStepRule extends StepRuleData { references: string[]; falsePositives: string[]; tags: string[]; + timeline: FieldValueTimeline; threats: IMitreEnterpriseAttack[]; } export interface DefineStepRule extends StepRuleData { - useIndicesConfig: string; index: string[]; queryBar: FieldValueQueryBar; } @@ -108,6 +109,8 @@ export interface AboutStepRuleJson { references: string[]; false_positives: string[]; tags: string[]; + timeline_id?: string; + timeline_title?: string; threats: IMitreEnterpriseAttack[]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx index 092c2463419d1..aafeea6465fb3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -16,8 +16,6 @@ import { hostDetailsPagePath } from '../types'; import { type } from './utils'; import { useMountAppended } from '../../../utils/use_mount_appended'; -jest.mock('../../../lib/kibana'); - jest.mock('../../../containers/source', () => ({ indicesExistOrDataTemporarilyUnavailable: () => true, WithSource: ({ diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index 00dcb5908a98b..065d91b3fc2fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -6,21 +6,23 @@ import { mount } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { ActionCreator } from 'typescript-fsa'; +import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; import '../../mock/match_media'; import { mocksSource } from '../../containers/source/mock'; import { wait } from '../../lib/helpers'; -import { TestProviders } from '../../mock'; +import { apolloClientObservable, TestProviders, mockGlobalState } from '../../mock'; import { InputsModelId } from '../../store/inputs/constants'; import { SiemNavigation } from '../../components/navigation'; +import { inputsActions } from '../../store/inputs'; +import { State, createStore } from '../../store'; import { HostsComponentProps } from './types'; import { Hosts } from './hosts'; - -jest.mock('../../lib/kibana'); +import { HostsTabs } from './hosts_tabs'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -138,4 +140,58 @@ describe('Hosts - rendering', () => { wrapper.update(); expect(wrapper.find(SiemNavigation).exists()).toBe(true); }); + + test('it should add the new filters after init', async () => { + const newFilters: esFilters.Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'host.name': 'ItRocks', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"query": {"bool": {"filter": [{"bool": {"should": [{"match_phrase": {"host.name": "ItRocks"}}],"minimum_should_match": 1}}]}}}', + }, + }, + ]; + localSource[0].result.data.source.status.indicesExist = true; + const myState: State = mockGlobalState; + const myStore = createStore(myState, apolloClientObservable); + const wrapper = mount( + + + + + + + + ); + await wait(); + wrapper.update(); + + myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); + wrapper.update(); + expect(wrapper.find(HostsTabs).props().filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 6b69f06b97b83..2c475e4ba6ac5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -41,7 +41,7 @@ import { HostsTableType } from '../../store/hosts/model'; const KpiHostsComponentManage = manageQuery(KpiHostsComponent); -const HostsComponent = React.memo( +export const HostsComponent = React.memo( ({ deleteQuery, isInitializing, @@ -56,13 +56,12 @@ const HostsComponent = React.memo( const capabilities = React.useContext(MlCapabilitiesContext); const kibana = useKibana(); const { tabName } = useParams(); - const hostsFilters = React.useMemo(() => { if (tabName === HostsTableType.alerts) { return filters.length > 0 ? [...filters, ...filterAlertsHosts] : filterAlertsHosts; } return filters; - }, [tabName]); + }, [tabName, filters]); const narrowDateRange = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index d624631c1feae..9a9d1cf085eb9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -5,9 +5,8 @@ */ import { shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { ActionCreator } from 'typescript-fsa'; @@ -28,8 +27,6 @@ const pop: Action = 'POP'; type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; -jest.mock('../../../lib/kibana'); - // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../components/search_bar', () => ({ @@ -131,7 +128,7 @@ describe('Ip Details', () => { test('it matches the snapshot', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); test('it renders ipv6 headline', async () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx index 335bb62c5c852..3a22e800d893f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx @@ -6,17 +6,18 @@ import { mount } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import '../../mock/match_media'; - +import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; import { mocksSource } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; +import { TestProviders, mockGlobalState, apolloClientObservable } from '../../mock'; +import { State, createStore } from '../../store'; +import { inputsActions } from '../../store/inputs'; import { Network } from './network'; - -jest.mock('../../lib/kibana'); +import { NetworkRoutes } from './navigation'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -113,4 +114,58 @@ describe('rendering - rendering', () => { wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); + + test('it should add the new filters after init', async () => { + const newFilters: esFilters.Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'host.name': 'ItRocks', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"query": {"bool": {"filter": [{"bool": {"should": [{"match_phrase": {"host.name": "ItRocks"}}],"minimum_should_match": 1}}]}}}', + }, + }, + ]; + localSource[0].result.data.source.status.indicesExist = true; + const myState: State = mockGlobalState; + const myStore = createStore(myState, apolloClientObservable); + const wrapper = mount( + + + + + + + + ); + await new Promise(resolve => setTimeout(resolve)); + wrapper.update(); + + myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); + wrapper.update(); + expect(wrapper.find(NetworkRoutes).props().filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx index c39935742a2e0..ad3513a6f529e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx @@ -59,7 +59,8 @@ const NetworkComponent = React.memo( return filters.length > 0 ? [...filters, ...filterAlertsNetwork] : filterAlertsNetwork; } return filters; - }, [tabName]); + }, [tabName, filters]); + const narrowDateRange = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx index 300df4a742adf..eff61bf6a9710 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; -import * as React from 'react'; +import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { MemoryRouter } from 'react-router-dom'; @@ -14,8 +14,6 @@ import { TestProviders } from '../../mock'; import { mocksSource } from '../../containers/source/mock'; import { Overview } from './index'; -jest.mock('../../lib/kibana'); - let localSource: Array<{ request: {}; result: { diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx index 4dec9278ed6b0..86f702a8ad8a4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx @@ -17,7 +17,6 @@ import * as i18n from './translations'; const TimelinesContainer = styled.div` width: 100%; `; -TimelinesContainer.displayName = 'TimelinesContainer'; interface TimelinesProps { apolloClient: ApolloClient; @@ -27,7 +26,7 @@ type OwnProps = TimelinesProps; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; -export const TimelinesPage = React.memo(({ apolloClient }) => ( +const TimelinesPageComponent: React.FC = ({ apolloClient }) => ( <> @@ -44,4 +43,6 @@ export const TimelinesPage = React.memo(({ apolloClient }) => ( -)); +); + +export const TimelinesPage = React.memo(TimelinesPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx new file mode 100644 index 0000000000000..44624f497a91b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AppMountParameters, + CoreSetup, + CoreStart, + PluginInitializerContext, + Plugin as IPlugin, +} from '../../../../../src/core/public'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; +import { IUiActionsStart } from '../../../../../src/plugins/ui_actions/public'; + +export { AppMountParameters, CoreSetup, CoreStart, PluginInitializerContext }; + +export interface SetupPlugins { + home: HomePublicPluginSetup; +} +export interface StartPlugins { + data: DataPublicPluginStart; + embeddable: IEmbeddableStart; + inspector: InspectorStart; + uiActions: IUiActionsStart; +} +export type StartServices = CoreStart & StartPlugins; + +export type Setup = ReturnType; +export type Start = ReturnType; + +export class Plugin implements IPlugin { + public id = 'siem'; + public name = 'SIEM'; + constructor( + // @ts-ignore this is added to satisfy the New Platform typing constraint, + // but we're not leveraging any of its functionality yet. + private readonly initializerContext: PluginInitializerContext + ) {} + + public setup(core: CoreSetup, plugins: SetupPlugins) { + core.application.register({ + id: this.id, + title: this.name, + async mount(context, params) { + const [coreStart, pluginsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + + return renderApp(coreStart, pluginsStart as StartPlugins, params); + }, + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartPlugins) { + return {}; + } + + public stop() { + return {}; + } +} diff --git a/x-pack/legacy/plugins/siem/public/register_feature.ts b/x-pack/legacy/plugins/siem/public/register_feature.ts index 36c4e28dbd874..ca7a22408b6ff 100644 --- a/x-pack/legacy/plugins/siem/public/register_feature.ts +++ b/x-pack/legacy/plugins/siem/public/register_feature.ts @@ -4,19 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - FeatureCatalogueCategory, - FeatureCatalogueRegistryProvider, -} from 'ui/registry/feature_catalogue'; +import { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { APP_ID } from '../common/constants'; -const APP_ID = 'siem'; - -FeatureCatalogueRegistryProvider.register(() => ({ - id: 'siem', +// TODO(rylnd): move this into Plugin.setup once we're on NP +npSetup.plugins.home.featureCatalogue.register({ + id: APP_ID, title: 'SIEM', description: 'Explore security metrics and logs for events and alerts', icon: 'securityAnalyticsApp', path: `/app/${APP_ID}`, showOnHomePage: true, category: FeatureCatalogueCategory.DATA, -})); +}); diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/legacy/plugins/siem/public/routes.tsx index 0e9bcf5dc5bfa..cbb58a473e8ea 100644 --- a/x-pack/legacy/plugins/siem/public/routes.tsx +++ b/x-pack/legacy/plugins/siem/public/routes.tsx @@ -16,7 +16,7 @@ interface RouterProps { history: History; } -export const PageRouter: FC = memo(({ history }) => ( +const PageRouterComponent: FC = ({ history }) => ( @@ -25,4 +25,6 @@ export const PageRouter: FC = memo(({ history }) => ( -)); +); + +export const PageRouter = memo(PageRouterComponent); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx index 87b40c565c758..d1e76f75fca81 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useReducer } from 'react'; +import React, { FC, memo, useReducer } from 'react'; import { ManageRoutesSpyProps, RouteSpyState, RouteSpyAction } from './types'; import { RouterSpyStateContext, initRouteSpy } from './helpers'; -export const ManageRoutesSpy = memo(({ children }: ManageRoutesSpyProps) => { +const ManageRoutesSpyComponent: FC = ({ children }) => { const reducerSpyRoute = (state: RouteSpyState, action: RouteSpyAction) => { switch (action.type) { case 'updateRoute': @@ -28,4 +28,6 @@ export const ManageRoutesSpy = memo(({ children }: ManageRoutesSpyProps) => { {children} ); -}); +}; + +export const ManageRoutesSpy = memo(ManageRoutesSpyComponent); diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 9e24e93b0c391..0da44eec3aaa3 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -61,7 +61,7 @@ const allRulesNdJson = 'index.ts'; const walk = dir => { const list = fs.readdirSync(dir); return list.reduce((accum, file) => { - const fileWithDir = dir + '/' + file; + const fileWithDir = `${dir}/${file}`; const stat = fs.statSync(fileWithDir); if (stat && stat.isDirectory()) { return [...accum, ...walk(fileWithDir)]; diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index 59df643246835..7060a3f662914 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -25,6 +25,8 @@ import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_ import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; +import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route'; +import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route'; const APP_ID = 'siem'; @@ -50,6 +52,8 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy createRulesBulkRoute(__legacy); updateRulesBulkRoute(__legacy); deleteRulesBulkRoute(__legacy); + importRulesRoute(__legacy); + exportRulesRoute(__legacy); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals // POST /api/detection_engine/signals/status diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts new file mode 100644 index 0000000000000..bd73805600a33 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/camelcase */ +import { + NewCaseFormatted, + NewCommentFormatted, +} from '../../../../../../../x-pack/plugins/case/server'; +import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; + +// Temporary file to write mappings for case +// while Saved Object Mappings API is programmed for the NP +// See: https://github.com/elastic/kibana/issues/50309 + +export const caseSavedObjectType = 'case-workflow'; +export const caseCommentSavedObjectType = 'case-workflow-comment'; + +export const caseSavedObjectMappings: { + [caseSavedObjectType]: ElasticsearchMappingOf; +} = { + [caseSavedObjectType]: { + properties: { + assignees: { + properties: { + username: { + type: 'keyword', + }, + full_name: { + type: 'keyword', + }, + }, + }, + created_at: { + type: 'date', + }, + description: { + type: 'text', + }, + title: { + type: 'keyword', + }, + created_by: { + properties: { + username: { + type: 'keyword', + }, + full_name: { + type: 'keyword', + }, + }, + }, + state: { + type: 'keyword', + }, + tags: { + type: 'keyword', + }, + case_type: { + type: 'keyword', + }, + }, + }, +}; + +export const caseCommentSavedObjectMappings: { + [caseCommentSavedObjectType]: ElasticsearchMappingOf; +} = { + [caseCommentSavedObjectType]: { + properties: { + comment: { + type: 'text', + }, + created_at: { + type: 'date', + }, + created_by: { + properties: { + full_name: { + type: 'keyword', + }, + username: { + type: 'keyword', + }, + }, + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts new file mode 100644 index 0000000000000..cb358c15e5fad --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getIndexExists } from './get_index_exists'; + +class StatusCode extends Error { + status: number = -1; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +describe('get_index_exists', () => { + test('it should return a true if you have _shards', async () => { + const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 1 } }); + const indexExists = await getIndexExists(callWithRequest, 'some-index'); + expect(indexExists).toEqual(true); + }); + + test('it should return a false if you do NOT have _shards', async () => { + const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 0 } }); + const indexExists = await getIndexExists(callWithRequest, 'some-index'); + expect(indexExists).toEqual(false); + }); + + test('it should return a false if it encounters a 404', async () => { + const callWithRequest = jest.fn().mockImplementation(() => { + throw new StatusCode(404, 'I am a 404 error'); + }); + const indexExists = await getIndexExists(callWithRequest, 'some-index'); + expect(indexExists).toEqual(false); + }); + + test('it should reject if it encounters a non 404', async () => { + const callWithRequest = jest.fn().mockImplementation(() => { + throw new StatusCode(500, 'I am a 500 error'); + }); + await expect(getIndexExists(callWithRequest, 'some-index')).rejects.toThrow('I am a 500 error'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index ff65caa59a866..705f542b50124 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -4,15 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndicesExistsParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const getIndexExists = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest< + { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, + {}, + { _shards: { total: number } } + >, index: string ): Promise => { - return callWithRequest('indices.exists', { - index, - }); + try { + const response = await callWithRequest('search', { + index, + size: 0, + terminate_after: 1, + allow_no_indices: true, + }); + return response._shards.total > 0; + } catch (err) { + if (err.status === 404) { + return false; + } else { + throw err; + } + } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index edf196b96f5d0..8ed5333bd2a25 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -52,6 +52,7 @@ export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({ created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }); export const typicalPayload = (): Partial => ({ @@ -158,7 +159,7 @@ export const getPrivilegeRequest = (): ServerInjectOptions => ({ url: `${DETECTION_ENGINE_PRIVILEGES_URL}`, }); -interface FindHit { +export interface FindHit { page: number; perPage: number; total: number; @@ -175,7 +176,7 @@ export const getFindResult = (): FindHit => ({ export const getFindResultWithSingleHit = (): FindHit => ({ page: 1, perPage: 1, - total: 0, + total: 1, data: [getResult()], }); @@ -271,6 +272,7 @@ export const getResult = (): RuleAlertType => ({ outputIndex: '.siem-signals', savedId: 'some-id', timelineId: 'some-timeline-id', + timelineTitle: 'some-timeline-title', meta: { someMeta: 'someField' }, filters: [ { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json index afe9bac9d87fe..79fb136afd52a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json @@ -36,6 +36,9 @@ "timeline_id": { "type": "keyword" }, + "timeline_title": { + "type": "keyword" + }, "max_signals": { "type": "keyword" }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 256b341fca656..3d9719a7b248b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -74,6 +74,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou updated_at: updatedAt, references, timeline_id: timelineId, + timeline_title: timelineTitle, version, } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); @@ -112,6 +113,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou outputIndex: finalIndex, savedId, timelineId, + timelineTitle, meta, filters, ruleId: ruleIdOrUuid, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 094449a5f61ac..10dc14f7ed610 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -28,7 +28,9 @@ describe('create_rules', () => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => true), + callWithRequest: jest + .fn() + .mockImplementation((endpoint, params) => ({ _shards: { total: 1 } })), })); createRulesRoute(server); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 476d5b8a49ba2..cf8fb2a28288f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -44,6 +44,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -101,6 +102,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = outputIndex: finalIndex, savedId, timelineId, + timelineTitle, meta, filters, ruleId: ruleId != null ? ruleId : uuid.v4(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 11a076951fd8c..bab05b065f6f7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -24,6 +24,7 @@ import { import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; +import { BulkError } from '../utils'; describe('delete_rules', () => { let { server, alertsClient } = createMockServer(); @@ -83,10 +84,14 @@ describe('delete_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); const { payload } = await server.inject(getDeleteBulkRequest()); - const parsed = JSON.parse(payload); - expect(parsed).toEqual([ - { error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' }, - ]); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); }); test('returns 404 if actionClient is not available on the route', async () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts new file mode 100644 index 0000000000000..aa17946849027 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { ExportRulesRequest } from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; +import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; +import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; +import { getExportAll } from '../../rules/get_export_all'; + +export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: `${DETECTION_ENGINE_RULES_URL}/_export`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: exportRulesSchema, + query: exportRulesQuerySchema, + }, + }, + async handler(request: ExportRulesRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); + if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { + return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } else { + const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); + if (nonPackagedRulesCount > exportSizeLimit) { + return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } + } + + const exported = + request.payload?.objects != null + ? await getExportByObjectIds(alertsClient, request.payload.objects) + : await getExportAll(alertsClient); + + const response = request.query.exclude_export_details + ? headers.response(exported.rulesNdjson) + : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); + + return response + .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) + .header('Content-Type', 'application/ndjson'); + }, + }; +}; + +export const exportRulesRoute = (server: ServerFacade): void => { + server.route(createExportRulesRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts new file mode 100644 index 0000000000000..e312b5fc6bb10 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import Hapi from 'hapi'; +import { extname } from 'path'; +import { isFunction } from 'lodash/fp'; +import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRules } from '../../rules/create_rules'; +import { ImportRulesRequest } from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { readRules } from '../../rules/read_rules'; +import { getIndexExists } from '../../index/get_index_exists'; +import { + callWithRequestFactory, + getIndex, + createImportErrorObject, + transformImportError, + ImportSuccessError, +} from '../utils'; +import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; +import { ImportRuleAlertRest } from '../../types'; +import { transformOrImportError } from './utils'; +import { updateRules } from '../../rules/update_rules'; +import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; + +export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: `${DETECTION_ENGINE_RULES_URL}/_import`, + options: { + tags: ['access:siem'], + payload: { + maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'), + output: 'stream', + allow: 'multipart/form-data', + }, + validate: { + options: { + abortEarly: false, + }, + query: importRulesQuerySchema, + payload: importRulesPayloadSchema, + }, + }, + async handler(request: ImportRulesRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + const { filename } = request.payload.file.hapi; + const fileExtension = extname(filename).toLowerCase(); + if (fileExtension !== '.ndjson') { + return Boom.badRequest(`Invalid file extension ${fileExtension}`); + } + + const objectLimit = server.config().get('savedObjects.maxImportExportSize'); + const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); + const parsedObjects = await createPromiseFromStreams<[ImportRuleAlertRest | Error]>([ + readStream, + ]); + + const reduced = await parsedObjects.reduce>( + async (accum, parsedRule) => { + const existingImportSuccessError = await accum; + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + return createImportErrorObject({ + ruleId: '(unknown)', // TODO: Better handling where we know which ruleId is having issues with imports + statusCode: 400, + message: parsedRule.message, + existingImportSuccessError, + }); + } + + const { + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threats, + to, + type, + references, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + } = parsedRule; + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const callWithRequest = callWithRequestFactory(request, server); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + return createImportErrorObject({ + ruleId, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + existingImportSuccessError, + }); + } + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + const createdRule = await createRules({ + alertsClient, + actionsClient, + createdAt: new Date().toISOString(), + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + updatedAt: new Date().toISOString(), + references, + version, + }); + return transformOrImportError(ruleId, createdRule, existingImportSuccessError); + } else if (rule != null && request.query.overwrite) { + const updatedRule = await updateRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + version, + }); + return transformOrImportError(ruleId, updatedRule, existingImportSuccessError); + } else { + return existingImportSuccessError; + } + } catch (err) { + return transformImportError(ruleId, err, existingImportSuccessError); + } + }, + Promise.resolve({ + success: true, + success_count: 0, + errors: [], + }) + ); + return reduced; + }, + }; +}; + +export const importRulesRoute = (server: ServerFacade): void => { + server.route(createImportRulesRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 9ae2941e6e5f2..2410dcee203f6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -23,6 +23,7 @@ import { } from '../__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; +import { BulkError } from '../utils'; describe('update_rules_bulk', () => { let { server, alertsClient, actionsClient } = createMockServer(); @@ -58,10 +59,14 @@ describe('update_rules_bulk', () => { actionsClient.update.mockResolvedValue(updateActionResult()); alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getUpdateBulkRequest()); - const parsed = JSON.parse(payload); - expect(parsed).toEqual([ - { error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' }, - ]); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); }); test('returns 404 if actionClient is not available on the route', async () => { @@ -125,10 +130,14 @@ describe('update_rules_bulk', () => { payload: [typicalPayload()], }; const { payload } = await server.inject(request); - const parsed = JSON.parse(payload); - expect(parsed).toEqual([ - { error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' }, - ]); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); }); test('returns 200 if type is query', async () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index b30b6c791522b..180a75bdaaeea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -50,6 +50,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -82,6 +83,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou outputIndex, savedId, timelineId, + timelineTitle, meta, filters, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index ec3d9514fa5db..6db8a8902915a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -38,6 +38,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -77,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { outputIndex, savedId, timelineId, + timelineTitle, meta, filters, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index b1f61d11458fe..c1b4c7de73f68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -14,11 +14,15 @@ import { transformTags, getIdBulkError, transformOrBulkError, + transformRulesToNdjson, + transformAlertsToRules, + transformOrImportError, } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { OutputRuleAlertRest } from '../../types'; -import { BulkError } from '../utils'; +import { BulkError, ImportSuccessError } from '../utils'; +import { sampleRule } from '../../signals/__mocks__/es_results'; describe('utils', () => { describe('transformAlertToRule', () => { @@ -79,6 +83,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -141,6 +146,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -205,6 +211,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -269,6 +276,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -331,6 +339,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -396,6 +405,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -461,6 +471,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -526,6 +537,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -642,6 +654,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual({ @@ -714,6 +727,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual(expected); @@ -746,8 +760,8 @@ describe('utils', () => { test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { const error = getIdBulkError({ id: '123', ruleId: undefined }); const expected: BulkError = { - id: '123', - error: { message: 'id: "123" not found', statusCode: 404 }, + rule_id: '123', + error: { message: 'id: "123" not found', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -755,8 +769,8 @@ describe('utils', () => { test('outputs message about id not being found if only id is defined and ruleId is null', () => { const error = getIdBulkError({ id: '123', ruleId: null }); const expected: BulkError = { - id: '123', - error: { message: 'id: "123" not found', statusCode: 404 }, + rule_id: '123', + error: { message: 'id: "123" not found', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -764,8 +778,8 @@ describe('utils', () => { test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { const error = getIdBulkError({ id: undefined, ruleId: 'rule-id-123' }); const expected: BulkError = { - id: 'rule-id-123', - error: { message: 'rule_id: "rule-id-123" not found', statusCode: 404 }, + rule_id: 'rule-id-123', + error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -773,8 +787,8 @@ describe('utils', () => { test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { const error = getIdBulkError({ id: null, ruleId: 'rule-id-123' }); const expected: BulkError = { - id: 'rule-id-123', - error: { message: 'rule_id: "rule-id-123" not found', statusCode: 404 }, + rule_id: 'rule-id-123', + error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -782,8 +796,8 @@ describe('utils', () => { test('outputs message about both being not defined when both are undefined', () => { const error = getIdBulkError({ id: undefined, ruleId: undefined }); const expected: BulkError = { - id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -791,8 +805,8 @@ describe('utils', () => { test('outputs message about both being not defined when both are null', () => { const error = getIdBulkError({ id: null, ruleId: null }); const expected: BulkError = { - id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -800,8 +814,8 @@ describe('utils', () => { test('outputs message about both being not defined when id is null and ruleId is undefined', () => { const error = getIdBulkError({ id: null, ruleId: undefined }); const expected: BulkError = { - id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -809,8 +823,8 @@ describe('utils', () => { test('outputs message about both being not defined when id is undefined and ruleId is null', () => { const error = getIdBulkError({ id: undefined, ruleId: null }); const expected: BulkError = { - id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, }; expect(error).toEqual(expected); }); @@ -875,6 +889,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual(expected); @@ -882,10 +897,279 @@ describe('utils', () => { test('returns 500 if the data is not of type siem alert', () => { const output = transformOrBulkError('rule-1', { data: [{ random: 1 }] }); - expect(output).toEqual({ - id: 'rule-1', - error: { message: 'Internal error transforming', statusCode: 500 }, + const expected: BulkError = { + rule_id: 'rule-1', + error: { message: 'Internal error transforming', status_code: 500 }, + }; + expect(output).toEqual(expected); + }); + }); + + describe('transformRulesToNdjson', () => { + test('if rules are empty it returns an empty string', () => { + const ruleNdjson = transformRulesToNdjson([]); + expect(ruleNdjson).toEqual(''); + }); + + test('single rule will transform with new line ending character for ndjson', () => { + const rule = sampleRule(); + const ruleNdjson = transformRulesToNdjson([rule]); + expect(ruleNdjson.endsWith('\n')).toBe(true); + }); + + test('multiple rules will transform with two new line ending characters for ndjson', () => { + const result1 = sampleRule(); + const result2 = sampleRule(); + result2.id = 'some other id'; + result2.rule_id = 'some other id'; + result2.name = 'Some other rule'; + + const ruleNdjson = transformRulesToNdjson([result1, result2]); + // this is how we count characters in JavaScript :-) + const count = ruleNdjson.split('\n').length - 1; + expect(count).toBe(2); + }); + + test('you can parse two rules back out without errors', () => { + const result1 = sampleRule(); + const result2 = sampleRule(); + result2.id = 'some other id'; + result2.rule_id = 'some other id'; + result2.name = 'Some other rule'; + + const ruleNdjson = transformRulesToNdjson([result1, result2]); + const ruleStrings = ruleNdjson.split('\n'); + const reParsed1 = JSON.parse(ruleStrings[0]); + const reParsed2 = JSON.parse(ruleStrings[1]); + expect(reParsed1).toEqual(result1); + expect(reParsed2).toEqual(result2); + }); + }); + + describe('transformAlertsToRules', () => { + test('given an empty array returns an empty array', () => { + expect(transformAlertsToRules([])).toEqual([]); + }); + + test('given single alert will return the alert transformed', () => { + const result1 = getResult(); + const transformed = transformAlertsToRules([result1]); + expect(transformed).toEqual([ + { + created_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + max_signals: 100, + meta: { someMeta: 'someField' }, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + risk_score: 50, + rule_id: 'rule-1', + saved_id: 'some-id', + severity: 'high', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + to: 'now', + type: 'query', + updated_at: '2019-12-13T16:40:33.400Z', + updated_by: 'elastic', + version: 1, + }, + ]); + }); + + test('given two alerts will return the two alerts transformed', () => { + const result1 = getResult(); + const result2 = getResult(); + result2.id = 'some other id'; + result2.params.ruleId = 'some other id'; + + const transformed = transformAlertsToRules([result1, result2]); + expect(transformed).toEqual([ + { + created_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + max_signals: 100, + meta: { someMeta: 'someField' }, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + risk_score: 50, + rule_id: 'rule-1', + saved_id: 'some-id', + severity: 'high', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + to: 'now', + type: 'query', + updated_at: '2019-12-13T16:40:33.400Z', + updated_by: 'elastic', + version: 1, + }, + { + created_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: 'some other id', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + max_signals: 100, + meta: { someMeta: 'someField' }, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + risk_score: 50, + rule_id: 'some other id', + saved_id: 'some-id', + severity: 'high', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + to: 'now', + type: 'query', + updated_at: '2019-12-13T16:40:33.400Z', + updated_by: 'elastic', + version: 1, + }, + ]); + }); + }); + + describe('transformOrImportError', () => { + test('returns 1 given success if the alert is an alert type and the existing success count is 0', () => { + const output = transformOrImportError('rule-1', getResult(), { + success: true, + success_count: 0, + errors: [], + }); + const expected: ImportSuccessError = { + success: true, + errors: [], + success_count: 1, + }; + expect(output).toEqual(expected); + }); + + test('returns 2 given successes if the alert is an alert type and the existing success count is 1', () => { + const output = transformOrImportError('rule-1', getResult(), { + success: true, + success_count: 1, + errors: [], }); + const expected: ImportSuccessError = { + success: true, + errors: [], + success_count: 2, + }; + expect(output).toEqual(expected); + }); + + test('returns 1 error and success of false if the data is not of type siem alert', () => { + const output = transformOrImportError( + 'rule-1', + { data: [{ random: 1 }] }, + { + success: true, + success_count: 1, + errors: [], + } + ); + const expected: ImportSuccessError = { + success: false, + errors: [ + { + rule_id: 'rule-1', + error: { + message: 'Internal error transforming', + status_code: 500, + }, + }, + ], + success_count: 1, + }; + expect(output).toEqual(expected); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index b9bf3f8a942fc..21972905a5063 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -9,7 +9,13 @@ import { pickBy } from 'lodash/fp'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, isAlertType, isAlertTypes } from '../../rules/types'; import { OutputRuleAlertRest } from '../../types'; -import { createBulkErrorObject, BulkError } from '../utils'; +import { + createBulkErrorObject, + BulkError, + createSuccessObject, + ImportSuccessError, + createImportErrorObject, +} from '../utils'; export const getIdError = ({ id, @@ -85,6 +91,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial>): string => { + if (rules.length !== 0) { + const rulesString = rules.map(rule => JSON.stringify(rule)).join('\n'); + return `${rulesString}\n`; + } else { + return ''; + } +}; + +export const transformAlertsToRules = ( + alerts: RuleAlertType[] +): Array> => { + return alerts.map(alert => transformAlertToRule(alert)); +}; + export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { if (isAlertTypes(findResults.data)) { findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); @@ -127,3 +149,20 @@ export const transformOrBulkError = ( }); } }; + +export const transformOrImportError = ( + ruleId: string, + alert: unknown, + existingImportSuccessError: ImportSuccessError +): ImportSuccessError => { + if (isAlertType(alert)) { + return createSuccessObject(existingImportSuccessError); + } else { + return createImportErrorObject({ + ruleId, + statusCode: 500, + message: 'Internal error transforming', + existingImportSuccessError, + }); + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 04a72135d3f00..74eb4d6c8e918 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -1076,9 +1076,7 @@ describe('add prepackaged rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - addPrepackagedRulesSchema.validate< - Partial & { meta: string }> - >({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1099,7 +1097,7 @@ describe('add prepackaged rules schema', () => { ).toBeFalsy(); }); - test('validates with timeline_id', () => { + test('validates with timeline_id and timeline_title', () => { expect( addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', @@ -1117,7 +1115,131 @@ describe('add prepackaged rules schema', () => { language: 'kuery', version: 1, timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }).error ).toBeFalsy(); }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: 'timeline-id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: 'timeline-id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: 'timeline-id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index c993b05cb5f29..49907b4a975e6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -21,6 +21,7 @@ import { language, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, @@ -63,6 +64,7 @@ export const addPrepackagedRulesSchema = Joi.object({ otherwise: Joi.forbidden(), }), timeline_id, + timeline_title, meta, risk_score: risk_score.required(), max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 8dc00b66e97a3..87916bea60649 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -1024,7 +1024,7 @@ describe('create rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - createRulesSchema.validate & { meta: string }>>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1045,7 +1045,7 @@ describe('create rules schema', () => { ).toBeFalsy(); }); - test('timeline_id validates', () => { + test('validates with timeline_id and timeline_title', () => { expect( createRulesSchema.validate>({ rule_id: 'rule-1', @@ -1062,8 +1062,122 @@ describe('create rules schema', () => { references: ['index-1'], query: 'some query', language: 'kuery', - timeline_id: 'some_id', + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }).error ).toBeFalsy(); }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index 614451312d04d..df5c1694d6c78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -22,6 +22,7 @@ import { output_index, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, @@ -57,6 +58,7 @@ export const createRulesSchema = Joi.object({ otherwise: Joi.forbidden(), }), timeline_id, + timeline_title, meta, risk_score: risk_score.required(), max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts new file mode 100644 index 0000000000000..7850e3a733f09 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { exportRulesSchema, exportRulesQuerySchema } from './export_rules_schema'; +import { ExportRulesRequest } from '../../rules/types'; + +describe('create rules schema', () => { + describe('exportRulesSchema', () => { + test('null value or absent values validate', () => { + expect(exportRulesSchema.validate(null).error).toBeFalsy(); + }); + + test('empty object does not validate', () => { + expect( + exportRulesSchema.validate>({}).error + ).toBeTruthy(); + }); + + test('empty object array does validate', () => { + expect( + exportRulesSchema.validate>({ objects: [] }).error + ).toBeTruthy(); + }); + + test('array with rule_id validates', () => { + expect( + exportRulesSchema.validate>({ + objects: [{ rule_id: 'test-1' }], + }).error + ).toBeFalsy(); + }); + + test('array with id does not validate as we do not allow that on purpose since we export rule_id', () => { + expect( + exportRulesSchema.validate>({ + objects: [{ id: 'test-1' }], + }).error + ).toBeTruthy(); + }); + }); + + describe('exportRulesQuerySchema', () => { + test('default value for file_name is export.ndjson', () => { + expect( + exportRulesQuerySchema.validate>({}).value.file_name + ).toEqual('export.ndjson'); + }); + + test('default value for exclude_export_details is false', () => { + expect( + exportRulesQuerySchema.validate>({}).value + .exclude_export_details + ).toEqual(false); + }); + + test('file_name validates', () => { + expect( + exportRulesQuerySchema.validate>({ + file_name: 'test.ndjson', + }).error + ).toBeFalsy(); + }); + + test('file_name does not validate with a number', () => { + expect( + exportRulesQuerySchema.validate< + Partial & { file_name: number }> + >({ + file_name: 5, + }).error + ).toBeTruthy(); + }); + + test('exclude_export_details validates with a boolean true', () => { + expect( + exportRulesQuerySchema.validate>({ + exclude_export_details: true, + }).error + ).toBeFalsy(); + }); + + test('exclude_export_details does not validate with a weird string', () => { + expect( + exportRulesQuerySchema.validate< + Partial< + Omit & { + exclude_export_details: string; + } + > + >({ + exclude_export_details: 'blah', + }).error + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts new file mode 100644 index 0000000000000..a14d81604d9f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { objects, exclude_export_details, file_name } from './schemas'; +/* eslint-disable @typescript-eslint/camelcase */ + +export const exportRulesSchema = Joi.object({ + objects, +}) + .min(1) + .allow(null); + +export const exportRulesQuerySchema = Joi.object({ + file_name: file_name.default('export.ndjson'), + exclude_export_details: exclude_export_details.default(false), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts new file mode 100644 index 0000000000000..09bc7a70711ec --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -0,0 +1,1327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + importRulesSchema, + importRulesQuerySchema, + importRulesPayloadSchema, +} from './import_rules_schema'; +import { ThreatParams, RuleAlertParamsRest, ImportRuleAlertRest } from '../../types'; +import { ImportRulesRequest } from '../../rules/types'; + +describe('import rules schema', () => { + describe('importRulesSchema', () => { + test('empty objects do not validate', () => { + expect(importRulesSchema.validate>({}).error).toBeTruthy(); + }); + + test('made up values do not validate', () => { + expect( + importRulesSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[rule_id] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + interval: '5m', + index: ['index-1'], + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + risk_score: 50, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + test('You can send in an empty array to threats', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [], + }).error + ).toBeFalsy(); + }); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + threats: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('allows references to be sent as valid', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('defaults references to an array', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual([]); + }); + + test('references cannot be numbers', () => { + expect( + importRulesSchema.validate< + Partial> & { references: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error + ).toBeTruthy(); + }); + + test('indexes cannot be numbers', () => { + expect( + importRulesSchema.validate< + Partial> & { index: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('defaults interval to 5 min', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + type: 'query', + }).value.interval + ).toEqual('5m'); + }); + + test('defaults max signals to 100', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(100); + }); + + test('saved_id is required when type is saved_query and will not validate without out', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + }).error + ).toBeTruthy(); + }); + + test('saved_id is required when type is saved_query and validates with it', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type can have filters with it', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeFalsy(); + }); + + test('filters cannot be a string', () => { + expect( + importRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: 'some string', + }).error + ).toBeTruthy(); + }); + + test('language validates with kuery', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be negative', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be zero', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error + ).toBeTruthy(); + }); + + test('max_signals can be 1', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can optionally send in an array of tags', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: ['tag_1', 'tag_2'], + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of tags that are numbers', () => { + expect( + importRulesSchema.validate> & { tags: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + } + ).error + ).toBeTruthy(); + }); + + test('You cannot send in an array of threats that are missing "framework"', () => { + expect( + importRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('You cannot send in an array of threats that are missing "tactic"', () => { + expect( + importRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('You cannot send in an array of threats that are missing "techniques"', () => { + expect( + importRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + + test('You can optionally send in an array of false positives', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: ['false_1', 'false_2'], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of false positives that are numbers', () => { + expect( + importRulesSchema.validate< + Partial> & { false_positives: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: [5, 4], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You can optionally set the immutable to be true', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot set the immutable to be a number', () => { + expect( + importRulesSchema.validate< + Partial> & { immutable: number } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: 5, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You cannot set the risk_score to 101', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 101, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You cannot set the risk_score to -1', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: -1, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You can set the risk_score to 0', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 0, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set the risk_score to 100', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 100, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set meta to any object you want', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }).error + ).toBeFalsy(); + }); + + test('You cannot create meta as a string', () => { + expect( + importRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: 'should not work', + }).error + ).toBeTruthy(); + }); + + test('You can omit the query string when filters are present', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('validates with timeline_id and timeline_title', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', + }).error + ).toBeFalsy(); + }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('rule_id is required and you cannot get by with just id', () => { + expect( + importRulesSchema.validate>({ + id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('it validates with created_at, updated_at, created_by, updated_by values', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + created_at: '2020-01-09T06:15:24.749Z', + updated_at: '2020-01-09T06:15:24.749Z', + created_by: 'Braden Hassanabad', + updated_by: 'Evan Hassanabad', + }).error + ).toBeFalsy(); + }); + + test('it does not validate with epoch strings for created_at', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + created_at: '1578550728650', + updated_at: '2020-01-09T06:15:24.749Z', + created_by: 'Braden Hassanabad', + updated_by: 'Evan Hassanabad', + }).error + ).toBeTruthy(); + }); + + test('it does not validate with epoch strings for updated_at', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + created_at: '2020-01-09T06:15:24.749Z', + updated_at: '1578550728650', + created_by: 'Braden Hassanabad', + updated_by: 'Evan Hassanabad', + }).error + ).toBeTruthy(); + }); + }); + + describe('importRulesQuerySchema', () => { + test('overwrite gets a default value of false', () => { + expect( + importRulesQuerySchema.validate>({}).value.overwrite + ).toEqual(false); + }); + + test('overwrite validates with a boolean true', () => { + expect( + importRulesQuerySchema.validate>({ + overwrite: true, + }).error + ).toBeFalsy(); + }); + + test('overwrite does not validate with a weird string', () => { + expect( + importRulesQuerySchema.validate< + Partial< + Omit & { + overwrite: string; + } + > + >({ + overwrite: 'blah', + }).error + ).toBeTruthy(); + }); + }); + + describe('importRulesPayloadSchema', () => { + test('does not validate with an empty object', () => { + expect(importRulesPayloadSchema.validate({}).error).toBeTruthy(); + }); + + test('does validate with a file object', () => { + expect(importRulesPayloadSchema.validate({ file: {} }).error).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts new file mode 100644 index 0000000000000..df825c442fff6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + id, + created_at, + updated_at, + created_by, + updated_by, + enabled, + description, + false_positives, + filters, + from, + immutable, + index, + rule_id, + interval, + query, + language, + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threats, + references, + version, +} from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; + +/** + * Differences from this and the createRulesSchema are + * - rule_id is required + * - id is optional (but ignored in the import code - rule_id is exclusively used for imports) + * - created_at is optional (but ignored in the import code) + * - updated_at is optional (but ignored in the import code) + * - created_by is optional (but ignored in the import code) + * - updated_by is optional (but ignored in the import code) + */ +export const importRulesSchema = Joi.object({ + id, + description: description.required(), + enabled: enabled.default(true), + false_positives: false_positives.default([]), + filters, + from: from.required(), + rule_id: rule_id.required(), + immutable: immutable.default(false), + index, + interval: interval.default('5m'), + query: query.allow('').default(''), + language: language.default('kuery'), + output_index, + saved_id: saved_id.when('type', { + is: 'saved_query', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), + timeline_id, + timeline_title, + meta, + risk_score: risk_score.required(), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), + name: name.required(), + severity: severity.required(), + tags: tags.default([]), + to: to.required(), + type: type.required(), + threats: threats.default([]), + references: references.default([]), + version: version.default(1), + created_at, + updated_at, + created_by, + updated_by, +}); + +export const importRulesQuerySchema = Joi.object({ + overwrite: Joi.boolean().default(false), +}); + +export const importRulesPayloadSchema = Joi.object({ + file: Joi.object().required(), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 68d3166c74d6d..ecca661d2b856 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -9,7 +9,9 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ export const description = Joi.string(); export const enabled = Joi.boolean(); +export const exclude_export_details = Joi.boolean(); export const false_positives = Joi.array().items(Joi.string()); +export const file_name = Joi.string(); export const filters = Joi.array(); export const from = Joi.string(); export const immutable = Joi.boolean(); @@ -21,9 +23,19 @@ export const index = Joi.array() export const interval = Joi.string(); export const query = Joi.string(); export const language = Joi.string().valid('kuery', 'lucene'); +export const objects = Joi.array().items( + Joi.object({ + rule_id, + }).required() +); export const output_index = Joi.string(); export const saved_id = Joi.string(); export const timeline_id = Joi.string(); +export const timeline_title = Joi.string().when('timeline_id', { + is: Joi.exist(), + then: Joi.required(), + otherwise: Joi.forbidden(), +}); export const meta = Joi.object(); export const max_signals = Joi.number().greater(0); export const name = Joi.string(); @@ -70,7 +82,6 @@ export const threat_technique = Joi.object({ reference: threat_technique_reference.required(), }); export const threat_techniques = Joi.array().items(threat_technique.required()); - export const threats = Joi.array().items( Joi.object({ framework: threat_framework.required(), @@ -78,5 +89,12 @@ export const threats = Joi.array().items( techniques: threat_techniques.required(), }) ); - +export const created_at = Joi.string() + .isoDate() + .strict(); +export const updated_at = Joi.string() + .isoDate() + .strict(); +export const created_by = Joi.string(); +export const updated_by = Joi.string(); export const version = Joi.number().min(1); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index 1f00e0a13866a..f713840ab43f9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -867,7 +867,7 @@ describe('update rules schema', () => { ).toBeTruthy(); }); - test('timeline_id validates', () => { + test('validates with timeline_id and timeline_title', () => { expect( updateRulesSchema.validate>({ id: 'rule-1', @@ -881,7 +881,101 @@ describe('update rules schema', () => { type: 'saved_query', saved_id: 'some id', timeline_id: 'some-id', + timeline_title: 'some-title', }).error ).toBeFalsy(); }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'timeline-id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index afd8a5fce4833..9c3188738faea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -22,6 +22,7 @@ import { output_index, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, @@ -53,6 +54,7 @@ export const updateRulesSchema = Joi.object({ output_index, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index fa95c77f646d6..ffd0c791c5bb6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,15 @@ import Boom from 'boom'; -import { transformError, transformBulkError, BulkError } from './utils'; +import { + transformError, + transformBulkError, + BulkError, + createSuccessObject, + ImportSuccessError, + createImportErrorObject, + transformImportError, +} from './utils'; describe('utils', () => { describe('transformError', () => { @@ -63,8 +71,8 @@ describe('utils', () => { const boom = new Boom('some boom message', { statusCode: 400 }); const transformed = transformBulkError('rule-1', boom); const expected: BulkError = { - id: 'rule-1', - error: { message: 'some boom message', statusCode: 400 }, + rule_id: 'rule-1', + error: { message: 'some boom message', status_code: 400 }, }; expect(transformed).toEqual(expected); }); @@ -77,8 +85,8 @@ describe('utils', () => { }; const transformed = transformBulkError('rule-1', error); const expected: BulkError = { - id: 'rule-1', - error: { message: 'some message', statusCode: 403 }, + rule_id: 'rule-1', + error: { message: 'some message', status_code: 403 }, }; expect(transformed).toEqual(expected); }); @@ -90,8 +98,8 @@ describe('utils', () => { }; const transformed = transformBulkError('rule-1', error); const expected: BulkError = { - id: 'rule-1', - error: { message: 'some message', statusCode: 500 }, + rule_id: 'rule-1', + error: { message: 'some message', status_code: 500 }, }; expect(transformed).toEqual(expected); }); @@ -100,8 +108,168 @@ describe('utils', () => { const error: TypeError = new TypeError('I have a type error'); const transformed = transformBulkError('rule-1', error); const expected: BulkError = { - id: 'rule-1', - error: { message: 'I have a type error', statusCode: 400 }, + rule_id: 'rule-1', + error: { message: 'I have a type error', status_code: 400 }, + }; + expect(transformed).toEqual(expected); + }); + }); + + describe('createSuccessObject', () => { + test('it should increment the existing success object by 1', () => { + const success = createSuccessObject({ + success_count: 0, + success: true, + errors: [], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: true, + errors: [], + }; + expect(success).toEqual(expected); + }); + + test('it should increment the existing success object by 1 and not touch the boolean or errors', () => { + const success = createSuccessObject({ + success_count: 0, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + ], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + ], + }; + expect(success).toEqual(expected); + }); + }); + + describe('createImportErrorObject', () => { + test('it creates an error message and does not increment the success count', () => { + const error = createImportErrorObject({ + ruleId: 'some-rule-id', + statusCode: 400, + message: 'some-message', + existingImportSuccessError: { + success_count: 1, + success: true, + errors: [], + }, + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }; + expect(error).toEqual(expected); + }); + + test('appends a second error message and does not increment the success count', () => { + const error = createImportErrorObject({ + ruleId: 'some-rule-id', + statusCode: 400, + message: 'some-message', + existingImportSuccessError: { + success_count: 1, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + ], + }, + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + ], + }; + expect(error).toEqual(expected); + }); + }); + + describe('transformImportError', () => { + test('returns transformed object if it is a boom object', () => { + const boom = new Boom('some boom message', { statusCode: 400 }); + const transformed = transformImportError('rule-1', boom, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 400, message: 'some boom message' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + + test('returns a normal error if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformImportError('rule-1', error, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 403, message: 'some message' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + + test('returns a 500 if the status code is not set', () => { + const error: Error & { statusCode?: number } = { + name: 'some name', + message: 'some message', + }; + const transformed = transformImportError('rule-1', error, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 500, message: 'some message' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + + test('it detects a TypeError and returns a Boom status of 400', () => { + const error: TypeError = new TypeError('I have a type error'); + const transformed = transformImportError('rule-1', error, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 400, message: 'I have a type error' } }, + ], }; expect(transformed).toEqual(expected); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index d9a8efd673883..19cd972b60e1a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -27,12 +27,13 @@ export const transformError = (err: Error & { statusCode?: number }) => { }; export interface BulkError { - id: string; + rule_id: string; error: { - statusCode: number; + status_code: number; message: string; }; } + export const createBulkErrorObject = ({ ruleId, statusCode, @@ -43,14 +44,84 @@ export const createBulkErrorObject = ({ message: string; }): BulkError => { return { - id: ruleId, + rule_id: ruleId, error: { - statusCode, + status_code: statusCode, message, }, }; }; +export interface ImportSuccessError { + success: boolean; + success_count: number; + errors: BulkError[]; +} + +export const createSuccessObject = ( + existingImportSuccessError: ImportSuccessError +): ImportSuccessError => { + return { + success_count: existingImportSuccessError.success_count + 1, + success: existingImportSuccessError.success, + errors: existingImportSuccessError.errors, + }; +}; + +export const createImportErrorObject = ({ + ruleId, + statusCode, + message, + existingImportSuccessError, +}: { + ruleId: string; + statusCode: number; + message: string; + existingImportSuccessError: ImportSuccessError; +}): ImportSuccessError => { + return { + success: false, + errors: [ + ...existingImportSuccessError.errors, + createBulkErrorObject({ + ruleId, + statusCode, + message, + }), + ], + success_count: existingImportSuccessError.success_count, + }; +}; + +export const transformImportError = ( + ruleId: string, + err: Error & { statusCode?: number }, + existingImportSuccessError: ImportSuccessError +): ImportSuccessError => { + if (Boom.isBoom(err)) { + return createImportErrorObject({ + ruleId, + statusCode: err.output.statusCode, + message: err.message, + existingImportSuccessError, + }); + } else if (err instanceof TypeError) { + return createImportErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + existingImportSuccessError, + }); + } else { + return createImportErrorObject({ + ruleId, + statusCode: err.statusCode ?? 500, + message: err.message, + existingImportSuccessError, + }); + } +}; + export const transformBulkError = ( ruleId: string, err: Error & { statusCode?: number } @@ -76,13 +147,19 @@ export const transformBulkError = ( } }; -export const getIndex = (request: RequestFacade, server: ServerFacade): string => { +export const getIndex = ( + request: RequestFacade | Omit, + server: ServerFacade +): string => { const spaceId = server.plugins.spaces.getSpaceId(request); const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); return `${signalsIndex}-${spaceId}`; }; -export const callWithRequestFactory = (request: RequestFacade, server: ServerFacade) => { +export const callWithRequestFactory = ( + request: RequestFacade | Omit, + server: ServerFacade +) => { const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); return (endpoint: string, params: T, options?: U) => { return callWithRequest(request, endpoint, params, options); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 07cf0b0c716cc..d2f76907d7aa3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -19,6 +19,7 @@ export const createRules = async ({ language, savedId, timelineId, + timelineTitle, meta, filters, ruleId, @@ -56,6 +57,7 @@ export const createRules = async ({ outputIndex, savedId, timelineId, + timelineTitle, meta, filters, maxSignals, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts new file mode 100644 index 0000000000000..fce3c90ef18e7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -0,0 +1,375 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Readable } from 'stream'; +import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; +import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams'; +import { ImportRuleAlertRest } from '../types'; + +const readStreamToCompletion = (stream: Readable) => { + return createPromiseFromStreams([stream, createConcatStream([])]); +}; + +export const getOutputSample = (): Partial => ({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', +}); + +export const getSampleAsNdjson = (sample: Partial): string => { + return `${JSON.stringify(sample)}\n`; +}; + +describe('create_rules_stream_from_ndjson', () => { + describe('createRulesStreamFromNdJson', () => { + test('transforms an ndjson stream into a stream of rule objects', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push(getSampleAsNdjson(sample2)); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const result = await readStreamToCompletion(rulesObjectsStream); + expect(result).toEqual([ + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }, + { + rule_id: 'rule-2', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }, + ]); + }); + + test('skips empty lines', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push('\n'); + this.push(getSampleAsNdjson(sample2)); + this.push(''); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const result = await readStreamToCompletion(rulesObjectsStream); + expect(result).toEqual([ + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }, + { + rule_id: 'rule-2', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }, + ]); + }); + + test('filters the export details entry from the stream', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push(getSampleAsNdjson(sample2)); + this.push('{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n'); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const result = await readStreamToCompletion(rulesObjectsStream); + expect(result).toEqual([ + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }, + { + rule_id: 'rule-2', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }, + ]); + }); + + test('handles non parsable JSON strings and inserts the error as part of the return array', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push('{,,,,\n'); + this.push(getSampleAsNdjson(sample2)); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const result = await readStreamToCompletion(rulesObjectsStream); + const resultOrError = result as Error[]; + expect(resultOrError[0]).toEqual({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }); + expect(resultOrError[1].message).toEqual('Unexpected token , in JSON at position 1'); + expect(resultOrError[2]).toEqual({ + rule_id: 'rule-2', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }); + }); + + test('handles non-validated data', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push(`{}\n`); + this.push(getSampleAsNdjson(sample2)); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const result = await readStreamToCompletion(rulesObjectsStream); + const resultOrError = result as TypeError[]; + expect(resultOrError[0]).toEqual({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }); + expect(resultOrError[1].message).toEqual( + 'child "description" fails because ["description" is required]' + ); + expect(resultOrError[2]).toEqual({ + rule_id: 'rule-2', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + enabled: true, + false_positives: [], + immutable: false, + query: '', + language: 'kuery', + max_signals: 100, + tags: [], + threats: [], + references: [], + version: 1, + }); + }); + + test('non validated data is an instanceof TypeError', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push(`{}\n`); + this.push(getSampleAsNdjson(sample2)); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const result = await readStreamToCompletion(rulesObjectsStream); + const resultOrError = result as TypeError[]; + expect(resultOrError[1] instanceof TypeError).toEqual(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts new file mode 100644 index 0000000000000..6d58171a3245d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Readable, Transform } from 'stream'; +import { has, isString } from 'lodash/fp'; +import { ImportRuleAlertRest } from '../types'; +import { + createSplitStream, + createMapStream, + createFilterStream, + createConcatStream, +} from '../../../../../../../../src/legacy/utils/streams'; +import { importRulesSchema } from '../routes/schemas/import_rules_schema'; + +export interface RulesObjectsExportResultDetails { + /** number of successfully exported objects */ + exportedCount: number; +} + +export const parseNdjsonStrings = (): Transform => { + return createMapStream((ndJsonStr: string) => { + if (isString(ndJsonStr) && ndJsonStr.trim() !== '') { + try { + return JSON.parse(ndJsonStr); + } catch (err) { + return err; + } + } + }); +}; + +export const filterExportedCounts = (): Transform => { + return createFilterStream( + obj => obj != null && !has('exported_count', obj) + ); +}; + +export const validateRules = (): Transform => { + return createMapStream((obj: ImportRuleAlertRest) => { + if (!(obj instanceof Error)) { + const validated = importRulesSchema.validate(obj); + if (validated.error != null) { + return new TypeError(validated.error.message); + } else { + return validated.value; + } + } else { + return obj; + } + }); +}; + +// Adaptation from: saved_objects/import/create_limit_stream.ts +export const createLimitStream = (limit: number): Transform => { + let counter = 0; + return new Transform({ + objectMode: true, + async transform(obj, _, done) { + if (counter >= limit) { + return done(new Error(`Can't import more than ${limit} rules`)); + } + counter++; + done(undefined, obj); + }, + }); +}; + +// TODO: Capture both the line number and the rule_id if you have that information for the error message +// eventually and then pass it down so we can give error messages on the line number + +/** + * Inspiration and the pattern of code followed is from: + * saved_objects/lib/create_saved_objects_stream_from_ndjson.ts + */ +export const createRulesStreamFromNdJson = ( + ndJsonStream: Readable, + ruleLimit: number +): Transform => { + return ndJsonStream + .pipe(createSplitStream('\n')) + .pipe(parseNdjsonStrings()) + .pipe(filterExportedCounts()) + .pipe(validateRules()) + .pipe(createLimitStream(ruleLimit)) + .pipe(createConcatStream([])); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts index 5f69082e3fc71..e193e123f4281 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FindResult } from '../../../../../alerting/server/alerts_client'; import { SIGNALS_ID } from '../../../../common/constants'; -import { FindRuleParams, RuleAlertType } from './types'; +import { FindRuleParams } from './types'; export const getFilter = (filter: string | null | undefined) => { if (filter == null) { @@ -23,7 +24,7 @@ export const findRules = async ({ filter, sortField, sortOrder, -}: FindRuleParams) => { +}: FindRuleParams): Promise => { return alertsClient.find({ options: { fields, @@ -33,10 +34,5 @@ export const findRules = async ({ sortOrder, sortField, }, - }) as Promise<{ - page: number; - perPage: number; - total: number; - data: RuleAlertType[]; - }>; + }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index bb28a5575f51e..dc308263baab6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -11,7 +11,13 @@ import { getFindResultWithSingleHit, getFindResultWithMultiHits, } from '../routes/__mocks__/request_responses'; -import { getExistingPrepackagedRules } from './get_existing_prepackaged_rules'; +import { + getExistingPrepackagedRules, + getNonPackagedRules, + getRules, + getRulesCount, + getNonPackagedRulesCount, +} from './get_existing_prepackaged_rules'; describe('get_existing_prepackaged_rules', () => { afterEach(() => { @@ -33,9 +39,11 @@ describe('get_existing_prepackaged_rules', () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); + result1.params.immutable = true; result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; const result2 = getResult(); + result2.params.immutable = true; result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; alertsClient.find.mockResolvedValueOnce( @@ -56,12 +64,15 @@ describe('get_existing_prepackaged_rules', () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); + result1.params.immutable = true; result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; const result2 = getResult(); + result2.params.immutable = true; result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; const result3 = getResult(); + result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; alertsClient.find.mockResolvedValueOnce( @@ -87,12 +98,15 @@ describe('get_existing_prepackaged_rules', () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); + result1.params.immutable = true; result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; const result2 = getResult(); + result2.params.immutable = true; result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; const result3 = getResult(); + result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; alertsClient.find.mockResolvedValueOnce( @@ -111,4 +125,221 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([result1, result2, result3]); }); }); + + describe('getNonPackagedRules', () => { + test('should return a single item in a single page', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getNonPackagedRules({ + alertsClient: unsafeCast, + }); + expect(rules).toEqual([getResult()]); + }); + + test('should return 2 items over two pages, one per page', async () => { + const alertsClient = alertsClientMock.create(); + + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) + ); + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) + ); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getNonPackagedRules({ + alertsClient: unsafeCast, + }); + expect(rules).toEqual([result1, result2]); + }); + + test('should return 3 items with over 3 pages one per page', async () => { + const alertsClient = alertsClientMock.create(); + + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result3 = getResult(); + result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) + ); + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) + ); + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + ); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getNonPackagedRules({ + alertsClient: unsafeCast, + }); + expect(rules).toEqual([result1, result2, result3]); + }); + + test('should return 3 items over 1 pages with all on one page', async () => { + const alertsClient = alertsClientMock.create(); + + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result3 = getResult(); + result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ + data: [result1, result2, result3], + perPage: 3, + page: 1, + total: 3, + }) + ); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getNonPackagedRules({ + alertsClient: unsafeCast, + }); + expect(rules).toEqual([result1, result2, result3]); + }); + }); + + describe('getRules', () => { + test('should return a single item in a single page', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getRules({ + alertsClient: unsafeCast, + filter: '', + }); + expect(rules).toEqual([getResult()]); + }); + + test('should return 2 items over two pages, one per page', async () => { + const alertsClient = alertsClientMock.create(); + + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) + ); + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) + ); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getRules({ + alertsClient: unsafeCast, + filter: '', + }); + expect(rules).toEqual([result1, result2]); + }); + + test('should return 3 items with over 3 pages one per page', async () => { + const alertsClient = alertsClientMock.create(); + + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result3 = getResult(); + result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) + ); + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) + ); + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + ); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getRules({ + alertsClient: unsafeCast, + filter: '', + }); + expect(rules).toEqual([result1, result2, result3]); + }); + + test('should return 3 items over 1 pages with all on one page', async () => { + const alertsClient = alertsClientMock.create(); + + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + + const result3 = getResult(); + result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ + data: [result1, result2, result3], + perPage: 3, + page: 1, + total: 3, + }) + ); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getRules({ + alertsClient: unsafeCast, + filter: '', + }); + expect(rules).toEqual([result1, result2, result3]); + }); + }); + + describe('getRulesCount', () => { + test('it returns a count', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getRulesCount({ + alertsClient: unsafeCast, + filter: '', + }); + expect(rules).toEqual(1); + }); + }); + + describe('getNonPackagedRulesCount', () => { + test('it returns a count', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rules = await getNonPackagedRulesCount({ + alertsClient: unsafeCast, + }); + expect(rules).toEqual(1); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index fa2e2124d0539..b7ab6a97634a8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -9,18 +9,46 @@ import { AlertsClient } from '../../../../../alerting'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; -export const DEFAULT_PER_PAGE: number = 100; +export const DEFAULT_PER_PAGE = 100; +export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`; +export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; -export const getExistingPrepackagedRules = async ({ +export const getNonPackagedRulesCount = async ({ + alertsClient, +}: { + alertsClient: AlertsClient; +}): Promise => { + return getRulesCount({ alertsClient, filter: FILTER_NON_PREPACKED_RULES }); +}; + +export const getRulesCount = async ({ + alertsClient, + filter, +}: { + alertsClient: AlertsClient; + filter: string; +}): Promise => { + const firstRule = await findRules({ + alertsClient, + filter, + perPage: 1, + page: 1, + }); + return firstRule.total; +}; + +export const getRules = async ({ alertsClient, perPage = DEFAULT_PER_PAGE, + filter, }: { alertsClient: AlertsClient; perPage?: number; + filter: string; }): Promise => { const firstPrepackedRules = await findRules({ alertsClient, - filter: `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`, + filter, perPage, page: 1, }); @@ -40,7 +68,7 @@ export const getExistingPrepackagedRules = async ({ // page index starts at 2 as we already got the first page and we have more pages to go return findRules({ alertsClient, - filter: `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`, + filter, perPage, page: page + 2, }); @@ -58,3 +86,31 @@ export const getExistingPrepackagedRules = async ({ } } }; + +export const getNonPackagedRules = async ({ + alertsClient, + perPage = DEFAULT_PER_PAGE, +}: { + alertsClient: AlertsClient; + perPage?: number; +}): Promise => { + return getRules({ + alertsClient, + perPage, + filter: FILTER_NON_PREPACKED_RULES, + }); +}; + +export const getExistingPrepackagedRules = async ({ + alertsClient, + perPage = DEFAULT_PER_PAGE, +}: { + alertsClient: AlertsClient; + perPage?: number; +}): Promise => { + return getRules({ + alertsClient, + perPage, + filter: FILTER_PREPACKED_RULES, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts new file mode 100644 index 0000000000000..eb9756af8fde1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { + getResult, + getFindResultWithSingleHit, + FindHit, +} from '../routes/__mocks__/request_responses'; +import { AlertsClient } from '../../../../../alerting'; +import { getExportAll } from './get_export_all'; + +describe('getExportAll', () => { + test('it exports everything from the alerts client', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const exports = await getExportAll(unsafeCast); + expect(exports).toEqual({ + rulesNdjson: + '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threats":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"techniques":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', + exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', + }); + }); + + test('it will export empty rules', async () => { + const alertsClient = alertsClientMock.create(); + const findResult: FindHit = { + page: 1, + perPage: 1, + total: 0, + data: [], + }; + + alertsClient.find.mockResolvedValue(findResult); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const exports = await getExportAll(unsafeCast); + expect(exports).toEqual({ + rulesNdjson: '', + exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts new file mode 100644 index 0000000000000..dca6eba4e6556 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.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 { AlertsClient } from '../../../../../alerting'; +import { getNonPackagedRules } from './get_existing_prepackaged_rules'; +import { getExportDetailsNdjson } from './get_export_details_ndjson'; +import { transformAlertsToRules, transformRulesToNdjson } from '../routes/rules/utils'; + +export const getExportAll = async ( + alertsClient: AlertsClient +): Promise<{ + rulesNdjson: string; + exportDetails: string; +}> => { + const ruleAlertTypes = await getNonPackagedRules({ alertsClient }); + const rules = transformAlertsToRules(ruleAlertTypes); + const rulesNdjson = transformRulesToNdjson(rules); + const exportDetails = getExportDetailsNdjson(rules); + return { rulesNdjson, exportDetails }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts new file mode 100644 index 0000000000000..a861d80a66fd5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { getExportByObjectIds, getRulesFromObjects, RulesErrors } from './get_export_by_object_ids'; +import { + getResult, + getFindResultWithSingleHit, + FindHit, +} from '../routes/__mocks__/request_responses'; +import { AlertsClient } from '../../../../../alerting'; + +describe('get_export_by_object_ids', () => { + describe('getExportByObjectIds', () => { + test('it exports object ids into an expected string with new line characters', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getExportByObjectIds(unsafeCast, objects); + expect(exports).toEqual({ + rulesNdjson: + '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threats":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"techniques":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', + exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', + }); + }); + + test('it does not export immutable rules', async () => { + const alertsClient = alertsClientMock.create(); + const result = getResult(); + result.params.immutable = true; + + const findResult: FindHit = { + page: 1, + perPage: 1, + total: 0, + data: [result], + }; + + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(findResult); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getExportByObjectIds(unsafeCast, objects); + expect(exports).toEqual({ + rulesNdjson: '', + exportDetails: + '{"exported_count":0,"missing_rules":[{"rule_id":"rule-1"}],"missing_rules_count":1}\n', + }); + }); + }); + + describe('getRulesFromObjects', () => { + test('it returns transformed rules from objects sent in', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getRulesFromObjects(unsafeCast, objects); + const expected: RulesErrors = { + missingRules: [], + rules: [ + { + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals', + max_signals: 100, + risk_score: 50, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + saved_id: 'some-id', + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + meta: { someMeta: 'someField' }, + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + version: 1, + }, + ], + }; + expect(exports).toEqual(expected); + }); + + test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => { + const alertsClient = alertsClientMock.create(); + const result = getResult(); + result.params.immutable = true; + + const findResult: FindHit = { + page: 1, + perPage: 1, + total: 0, + data: [result], + }; + + alertsClient.get.mockResolvedValue(result); + alertsClient.find.mockResolvedValue(findResult); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getRulesFromObjects(unsafeCast, objects); + const expected: RulesErrors = { + missingRules: [{ rule_id: 'rule-1' }], + rules: [], + }; + expect(exports).toEqual(expected); + }); + + test('it exports missing rules', async () => { + const alertsClient = alertsClientMock.create(); + + const findResult: FindHit = { + page: 1, + perPage: 1, + total: 0, + data: [], + }; + + alertsClient.get.mockRejectedValue({ output: { statusCode: 404 } }); + alertsClient.find.mockResolvedValue(findResult); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getRulesFromObjects(unsafeCast, objects); + const expected: RulesErrors = { + missingRules: [{ rule_id: 'rule-1' }], + rules: [], + }; + expect(exports).toEqual(expected); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts new file mode 100644 index 0000000000000..a5cf1bbfb7858 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertsClient } from '../../../../../alerting'; +import { getExportDetailsNdjson } from './get_export_details_ndjson'; +import { isAlertType } from '../rules/types'; +import { readRules } from './read_rules'; +import { transformRulesToNdjson, transformAlertToRule } from '../routes/rules/utils'; +import { OutputRuleAlertRest } from '../types'; + +export interface RulesErrors { + missingRules: Array<{ rule_id: string }>; + rules: Array>; +} + +export const getExportByObjectIds = async ( + alertsClient: AlertsClient, + objects: Array<{ rule_id: string }> +): Promise<{ + rulesNdjson: string; + exportDetails: string; +}> => { + const rulesAndErrors = await getRulesFromObjects(alertsClient, objects); + const rulesNdjson = transformRulesToNdjson(rulesAndErrors.rules); + const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules); + return { rulesNdjson, exportDetails }; +}; + +export const getRulesFromObjects = async ( + alertsClient: AlertsClient, + objects: Array<{ rule_id: string }> +): Promise => { + const alertsAndErrors = await objects.reduce>( + async (accumPromise, object) => { + const accum = await accumPromise; + const rule = await readRules({ alertsClient, ruleId: object.rule_id }); + if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { + const transformedRule = transformAlertToRule(rule); + return { + missingRules: accum.missingRules, + rules: [...accum.rules, transformedRule], + }; + } else { + return { + missingRules: [...accum.missingRules, { rule_id: object.rule_id }], + rules: accum.rules, + }; + } + }, + Promise.resolve({ + exportedCount: 0, + missingRules: [], + rules: [], + }) + ); + return alertsAndErrors; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts new file mode 100644 index 0000000000000..431b3776fd9e2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleRule } from '../signals/__mocks__/es_results'; +import { getExportDetailsNdjson } from './get_export_details_ndjson'; + +describe('getExportDetailsNdjson', () => { + test('it ends with a new line character', () => { + const rule = sampleRule(); + const details = getExportDetailsNdjson([rule]); + expect(details.endsWith('\n')).toEqual(true); + }); + + test('it exports a correct count given a single rule and no missing rules', () => { + const rule = sampleRule(); + const details = getExportDetailsNdjson([rule]); + const reParsed = JSON.parse(details); + expect(reParsed).toEqual({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + }); + }); + + test('it exports a correct count given a no rules and a single missing rule', () => { + const missingRule = { rule_id: 'rule-1' }; + const details = getExportDetailsNdjson([], [missingRule]); + const reParsed = JSON.parse(details); + expect(reParsed).toEqual({ + exported_count: 0, + missing_rules: [{ rule_id: 'rule-1' }], + missing_rules_count: 1, + }); + }); + + test('it exports a correct count given multiple rules and multiple missing rules', () => { + const rule1 = sampleRule(); + const rule2 = sampleRule(); + rule2.rule_id = 'some other id'; + rule2.id = 'some other id'; + + const missingRule1 = { rule_id: 'rule-1' }; + const missingRule2 = { rule_id: 'rule-2' }; + + const details = getExportDetailsNdjson([rule1, rule2], [missingRule1, missingRule2]); + const reParsed = JSON.parse(details); + expect(reParsed).toEqual({ + exported_count: 2, + missing_rules: [missingRule1, missingRule2], + missing_rules_count: 2, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts new file mode 100644 index 0000000000000..a39541d044bc3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OutputRuleAlertRest } from '../types'; + +export const getExportDetailsNdjson = ( + rules: Array>, + missingRules: Array<{ rule_id: string }> = [] +): string => { + const stringified = JSON.stringify({ + exported_count: rules.length, + missing_rules: missingRules, + missing_rules_count: missingRules.length, + }); + return `${stringified}\n`; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 9acfbf8c43221..9c3be64f71a0d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -26,6 +26,7 @@ export const installPrepackagedRules = async ( language, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -55,6 +56,7 @@ export const installPrepackagedRules = async ( outputIndex, savedId, timelineId, + timelineTitle, meta, filters, ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts index 4e370bfdc5bc9..49b3c5d6802b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -283,6 +283,7 @@ import rule273 from './splunk_detect_use_of_cmdexe_to_launch_script_interpreters import rule274 from './splunk_child_processes_of_spoolsvexe.json'; import rule275 from './splunk_detect_psexec_with_accepteula_flag.json'; import rule276 from './splunk_processes_created_by_netsh.json'; +import rule277 from './process_execution_via_wmi.json'; export const rawRules = [ rule1, @@ -561,4 +562,5 @@ export const rawRules = [ rule274, rule275, rule276, + rule277, ]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json index dee04ee4fea8a..00976ea21cd44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json @@ -9,7 +9,7 @@ "type": "query", "from": "now-6m", "to": "now", - "query": "(destination.port:6665 or destination.port:6666 or destination.port:6667 or destination.port:6668 or destination.port:6669) and not destination.ip:10.0.0.0/8 and not destination.ip:172.16.0.0/12 and not destination.ip:192.168.0.0/16", + "query": "(destination.port:20 or destination.port:21) and not destination.ip:10.0.0.0/8 and not destination.ip:172.16.0.0/12 and not destination.ip:192.168.0.0/16", "language": "kuery", "filters": [], "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json index c5a16bfef7248..69383d91ccbb9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json @@ -9,7 +9,7 @@ "type": "query", "from": "now-6m", "to": "now", - "query": "destination.port:3389 and not source.ip:10.0.0.0/8 and not source.ip:172.16.0.0/12 and not source.ip:192.168.0.0/16", + "query": "(destination.port:8080 or destination.port:3128) and not destination.ip:10.0.0.0/8 and not destination.ip:172.16.0.0/12 and not destination.ip:192.168.0.0/16", "language": "kuery", "filters": [], "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/process_execution_via_wmi.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/process_execution_via_wmi.json new file mode 100644 index 0000000000000..d6743c1ead4ac --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/process_execution_via_wmi.json @@ -0,0 +1,17 @@ +{ + "rule_id": "14ba7cd9-1489-459b-99a4-153c7a3f9abb", + "risk_score": 50, + "description": "Process Execution via WMI", + "immutable": true, + "interval": "5m", + "name": "Process Execution via WMI", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "process.name:scrcons.exe", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index 9c83ae924486d..e8fa0b562bd24 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Alert } from '../../../../../alerting/server/types'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; -import { RuleAlertType, ReadRuleParams, isAlertType } from './types'; +import { ReadRuleParams, isAlertType } from './types'; /** * This reads the rules through a cascade try of what is fastest to what is slowest. @@ -20,7 +21,7 @@ export const readRules = async ({ alertsClient, id, ruleId, -}: ReadRuleParams): Promise => { +}: ReadRuleParams): Promise => { if (id != null) { try { const rule = await alertsClient.get({ id }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 4f4c0da7127cd..4d9073e4b38d9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -5,6 +5,7 @@ */ import { get } from 'lodash/fp'; +import { Readable } from 'stream'; import { SIGNALS_ID } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting/server/alerts_client'; @@ -47,6 +48,24 @@ export interface BulkRulesRequest extends RequestFacade { payload: RuleAlertParamsRest[]; } +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + }; +} +export interface ImportRulesRequest extends Omit { + query: { overwrite: boolean }; + payload: { file: HapiReadableStream }; +} + +export interface ExportRulesRequest extends Omit { + payload: { objects: Array<{ rule_id: string }> | null | undefined }; + query: { + file_name: string; + exclude_export_details: boolean; + }; +} + export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index c9dac82b6eb8f..0fe4b15437af8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -74,6 +74,7 @@ export const updateRules = async ({ outputIndex, savedId, timelineId, + timelineTitle, meta, filters, from, @@ -118,6 +119,7 @@ export const updateRules = async ({ outputIndex, savedId, timelineId, + timelineTitle, meta, filters, index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh new file mode 100755 index 0000000000000..b46b5a0e80639 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +EXCLUDE_DETAILS=${1:-false} + +# Note: This file does not use jq on purpose for testing and pipe redirections + +# Example get all the rules except pre-packaged rules +# ./export_rules.sh + +# Example get the export details at the end +# ./export_rules.sh false + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh new file mode 100755 index 0000000000000..bed9753b1b6f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + + +# Uses a default if no argument is specified +RULES=${1:-./rules/export/ruleid_queries.json} +EXCLUDE_DETAILS=${2:-false} + +# Note: This file does not use jq on purpose for testing and pipe redirections + +# Example get all the rules except pre-packaged rules +# ./export_rules_by_rule_id.sh + +# Example get the export details at the end +# ./export_rules_by_rule_id.sh ./rules/export/ruleid_queries.json false +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS} \ + -d @${RULES} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh new file mode 100755 index 0000000000000..614024996cb39 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no arguments are specified +RULES=${1:-./rules/export/ruleid_queries.json} +FILENAME=${2:-test.ndjson} +EXCLUDE_DETAILS=${3:-false} + +# Example export to the file named test.ndjson +# ./export_rules_by_rule_id_to_file.sh + +# Example export to the file named test.ndjson with export details appended +# ./export_rules_by_rule_id_to_file.sh ./rules/export/ruleid_queries.json test.ndjson false +curl -s -k -OJ \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS}&file_name=${FILENAME}" \ + -d @${RULES} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh new file mode 100755 index 0000000000000..a45e8036d004e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +FILENAME=${1:-test.ndjson} +EXCLUDE_DETAILS=${2:-false} + +# Example export to the file named test.ndjson +# ./export_rules_to_file.sh + +# Example export to the file named test.ndjson with export details appended +# ./export_rules_to_file.sh test.ndjson false +curl -s -k -OJ \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS}&file_name=${FILENAME}" diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh new file mode 100755 index 0000000000000..5bb9ec589a7e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +RULES=${1:-./rules/import/multiple_ruleid_queries.ndjson} +OVERWRITE=${2:-true} + +# Example to import and overwrite everything from ./rules/import/multiple_ruleid_queries.ndjson +# ./import_rules.sh + +# Example to not overwrite everything if it exists from ./rules/import/multiple_ruleid_queries.ndjson +# ./import_rules.sh ./rules/import/multiple_ruleid_queries.ndjson false +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_import?overwrite=${OVERWRITE}" \ + --form file=@${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh new file mode 100755 index 0000000000000..dfc58bb5c1ab8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +RULES=${1:-./rules/import/multiple_ruleid_queries.ndjson} + +# Example: ./import_rules_no_overwrite.sh +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_import \ + --form file=@${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json new file mode 100644 index 0000000000000..fabc37d9f5766 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json @@ -0,0 +1,10 @@ +{ + "objects": [ + { + "rule_id": "query-rule-id-1" + }, + { + "rule_id": "query-rule-id-2" + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson new file mode 100644 index 0000000000000..a9de8b1e475a3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson @@ -0,0 +1,3 @@ +{"created_at":"2020-01-09T01:38:00.740Z","updated_at":"2020-01-09T01:38:00.740Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"6688f367-1aa2-4895-a5a8-b3701eecf57d","immutable":false,"interval":"5m","rule_id":"query-rule-id-1","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":1,"name":"Query with a rule id Number 1","query":"user.name: root or user.name: admin","references":[],"severity":"high","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1} +{"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-2","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1} +{"exported_count":2,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json index 2f995029447ff..eb87a14e0c688 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json @@ -7,5 +7,6 @@ "from": "now-6m", "to": "now", "query": "user.name: root or user.name: admin", - "timeline_id": "timeline-id" + "timeline_id": "timeline-id", + "timeline_title": "timeline_title" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json index 60095a0a6a833..e9d955f920571 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json @@ -21,7 +21,7 @@ } ], "enabled": false, - "immutable": true, + "immutable": false, "index": ["auditbeat-*", "filebeat-*"], "interval": "5m", "query": "user.name: root or user.name: admin", @@ -78,5 +78,6 @@ "Some plain text string here explaining why this is a valid thing to look out for" ], "timeline_id": "timeline_id", + "timeline_title": "timeline_title", "version": 1 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json index 2628b69eb064d..16d5d6cc2b36a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json @@ -78,5 +78,6 @@ "Some plain text string here explaining why this is a valid thing to look out for" ], "saved_id": "test-saved-id", - "timeline_id": "test-timeline-id" + "timeline_id": "test-timeline-id", + "timeline_title": "test-timeline-title" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/multiple_ruleid_queries_corrupted.ndjson b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/multiple_ruleid_queries_corrupted.ndjson new file mode 100644 index 0000000000000..94fc36ef6f7bf --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/multiple_ruleid_queries_corrupted.ndjson @@ -0,0 +1,4 @@ +{"created_at":"2020-01-09T01:38:00.740Z","updated_at":"2020-01-09T01:38:00.740Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"6688f367-1aa2-4895-a5a8-b3701eecf57d","immutable":false,"interval":"5m","rule_id":"query-rule-id-1","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":1,"name":"Query with a rule id Number 1","query":"user.name: root or user.name: admin","references":[],"severity":"high","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1}, +{"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-2","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1} +{"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-3","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1} +{"exported_count":2,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json index 4da285e5b09bf..7fc8de9fe8f9e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json @@ -78,5 +78,6 @@ "Some plain text string here explaining why this is a valid thing to look out for" ], "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title", "version": 42 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json index 8cfa3303f54a6..27dee7dd81463 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json @@ -1,4 +1,5 @@ { "rule_id": "query-rule-id", - "timeline_id": "other-timeline-id" + "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 5e50b65b51717..ede82a597b238 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -31,6 +31,7 @@ export const sampleRuleAlertParams = ( filters: undefined, savedId: undefined, timelineId: undefined, + timelineTitle: undefined, meta: undefined, threats: undefined, version: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 0a3526d32e511..1093ff3a8a462 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -34,6 +34,7 @@ export const buildRule = ({ false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, + timeline_title: ruleParams.timelineTitle, meta: ruleParams.meta, max_signals: ruleParams.maxSignals, risk_score: ruleParams.riskScore, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 87d31abbc5371..ab2c1733b04ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -42,6 +42,7 @@ export const signalRulesAlertType = ({ outputIndex: schema.nullable(schema.string()), savedId: schema.nullable(schema.string()), timelineId: schema.nullable(schema.string()), + timelineTitle: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index f4a8263da6ba4..968c7d9cb1cf0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -44,6 +44,7 @@ export interface RuleAlertParams { tags: string[]; to: string; timelineId: string | undefined | null; + timelineTitle: string | undefined | null; threats: ThreatParams[] | undefined | null; type: 'query' | 'saved_query'; version: number; @@ -60,6 +61,7 @@ export type RuleAlertParamsRest = Omit< | 'savedId' | 'riskScore' | 'timelineId' + | 'timelineTitle' | 'outputIndex' | 'updatedAt' | 'createdAt' @@ -68,6 +70,7 @@ export type RuleAlertParamsRest = Omit< false_positives: RuleAlertParams['falsePositives']; saved_id: RuleAlertParams['savedId']; timeline_id: RuleAlertParams['timelineId']; + timeline_title: RuleAlertParams['timelineTitle']; max_signals: RuleAlertParams['maxSignals']; risk_score: RuleAlertParams['riskScore']; output_index: RuleAlertParams['outputIndex']; @@ -81,4 +84,9 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { updated_by: string | undefined | null; }; +export type ImportRuleAlertRest = Omit & { + id: string | undefined | null; + rule_id: string; +}; + export type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 8a47aa2a27082..90ae79ef19d5b 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -60,7 +60,7 @@ export class Plugin { ], read: ['config'], }, - ui: ['show'], + ui: ['show', 'crud'], }, read: { api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts index 79a4eeb6dc48b..777471e209adc 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts @@ -99,7 +99,7 @@ export const setup = async (): Promise => { const tabs = ['snapshots', 'repositories']; testBed - .find('tab') + .find(`${tab}_tab`) .at(tabs.indexOf(tab)) .simulate('click'); }; @@ -360,7 +360,10 @@ export type TestSubjects = | 'state' | 'state.title' | 'state.value' - | 'tab' + | 'repositories_tab' + | 'snapshots_tab' + | 'policies_tab' + | 'restore_status_tab' | 'tableHeaderCell_durationInMillis_3' | 'tableHeaderCell_durationInMillis_3.tableHeaderSortButton' | 'tableHeaderCell_indices_4' diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts index d9f2c1b510a14..cb2e94df75609 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts @@ -95,6 +95,16 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setCleanupRepositoryResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 503 : 200; + + server.respondWith('POST', `${API_BASE_PATH}repositories/:name/cleanup`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + const setGetPolicyResponse = (response?: HttpResponse) => { server.respondWith('GET', `${API_BASE_PATH}policy/:name`, [ 200, @@ -113,6 +123,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadIndicesResponse, setAddPolicyResponse, setGetPolicyResponse, + setCleanupRepositoryResponse, }; }; diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index aa659441043ae..517c7a0059a7e 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -88,8 +88,15 @@ describe('', () => { test('should have 4 tabs', () => { const { find } = testBed; - expect(find('tab').length).toBe(4); - expect(find('tab').map(t => t.text())).toEqual([ + const tabs = [ + find('snapshots_tab'), + find('repositories_tab'), + find('policies_tab'), + find('restore_status_tab'), + ]; + + expect(tabs.length).toBe(4); + expect(tabs.map(t => t.text())).toEqual([ 'Snapshots', 'Repositories', 'Policies', diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts index 5900d53afa0b4..b9b26c5590324 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts @@ -157,3 +157,15 @@ export interface InvalidRepositoryVerification { } export type RepositoryVerification = ValidRepositoryVerification | InvalidRepositoryVerification; + +export interface SuccessfulRepositoryCleanup { + cleaned: true; + response: object; +} + +export interface FailedRepositoryCleanup { + cleaned: false; + error: object; +} + +export type RepositoryCleanup = FailedRepositoryCleanup | SuccessfulRepositoryCleanup; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts index 844394deb4f8d..481516479df4e 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts @@ -103,6 +103,7 @@ export const UIM_REPOSITORY_DELETE = 'repository_delete'; export const UIM_REPOSITORY_DELETE_MANY = 'repository_delete_many'; export const UIM_REPOSITORY_SHOW_DETAILS_CLICK = 'repository_show_details_click'; export const UIM_REPOSITORY_DETAIL_PANEL_VERIFY = 'repository_detail_panel_verify'; +export const UIM_REPOSITORY_DETAIL_PANEL_CLEANUP = 'repository_detail_panel_cleanup'; export const UIM_SNAPSHOT_LIST_LOAD = 'snapshot_list_load'; export const UIM_SNAPSHOT_SHOW_DETAILS_CLICK = 'snapshot_show_details_click'; export const UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB = 'snapshot_detail_panel_summary_tab'; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx index 35d5c0b610b3c..f89aa869b3366 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx @@ -150,7 +150,7 @@ export const SnapshotRestoreHome: React.FunctionComponent onSectionChange(tab.id)} isSelected={tab.id === section} key={tab.id} - data-test-subj="tab" + data-test-subj={tab.id.toLowerCase() + '_tab'} > {tab.name} diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx index c03162bae8bd2..0a3fcfc2ec6e7 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - EuiCodeEditor, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -19,6 +18,8 @@ import { EuiLink, EuiSpacer, EuiTitle, + EuiCodeBlock, + EuiText, } from '@elastic/eui'; import 'brace/theme/textmate'; @@ -28,12 +29,17 @@ import { documentationLinksService } from '../../../../services/documentation'; import { useLoadRepository, verifyRepository as verifyRepositoryRequest, + cleanupRepository as cleanupRepositoryRequest, } from '../../../../services/http'; import { textService } from '../../../../services/text'; import { linkToSnapshots, linkToEditRepository } from '../../../../services/navigation'; import { REPOSITORY_TYPES } from '../../../../../../common/constants'; -import { Repository, RepositoryVerification } from '../../../../../../common/types'; +import { + Repository, + RepositoryVerification, + RepositoryCleanup, +} from '../../../../../../common/types'; import { RepositoryDeleteProvider, SectionError, @@ -61,7 +67,9 @@ export const RepositoryDetails: React.FunctionComponent = ({ const { FormattedMessage } = i18n; const { error, data: repositoryDetails } = useLoadRepository(repositoryName); const [verification, setVerification] = useState(undefined); + const [cleanup, setCleanup] = useState(undefined); const [isLoadingVerification, setIsLoadingVerification] = useState(false); + const [isLoadingCleanup, setIsLoadingCleanup] = useState(false); const verifyRepository = async () => { setIsLoadingVerification(true); @@ -70,11 +78,20 @@ export const RepositoryDetails: React.FunctionComponent = ({ setIsLoadingVerification(false); }; - // Reset verification state when repository name changes, either from adjust URL or clicking + const cleanupRepository = async () => { + setIsLoadingCleanup(true); + const { data } = await cleanupRepositoryRequest(repositoryName); + setCleanup(data.cleanup); + setIsLoadingCleanup(false); + }; + + // Reset verification state and cleanup when repository name changes, either from adjust URL or clicking // into a different repository in table list. useEffect(() => { setVerification(undefined); setIsLoadingVerification(false); + setCleanup(undefined); + setIsLoadingCleanup(false); }, [repositoryName]); const renderBody = () => { @@ -231,6 +248,8 @@ export const RepositoryDetails: React.FunctionComponent = ({ {renderVerification()} + + {renderCleanup()} ); }; @@ -260,36 +279,13 @@ export const RepositoryDetails: React.FunctionComponent = ({ {verification ? ( - + {JSON.stringify( verification.valid ? verification.response : verification.error, null, 2 )} - setOptions={{ - showLineNumbers: false, - tabSize: 2, - maxLines: Infinity, - }} - editorProps={{ - $blockScrolling: Infinity, - }} - showGutter={false} - minLines={6} - aria-label={ - - } - /> + ) : null} @@ -318,6 +314,78 @@ export const RepositoryDetails: React.FunctionComponent = ({ ); + const renderCleanup = () => ( + <> + +

+ +

+
+ + +

+ +

+
+ {cleanup ? ( + <> + + {cleanup.cleaned ? ( +
+ +

+ +

+
+ + {JSON.stringify(cleanup.response, null, 2)} + +
+ ) : ( + +

+ {cleanup.error + ? JSON.stringify(cleanup.error) + : i18n.translate('xpack.snapshotRestore.repositoryDetails.cleanupUnknownError', { + defaultMessage: '503: Unknown error', + })} +

+
+ )} + + ) : null} + + + + + + ); + const renderFooter = () => { return ( diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx index 4b5270b44d593..1df06f67c35b1 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx @@ -96,6 +96,7 @@ export const RepositoryTable: React.FunctionComponent = ({ }, }, { + field: 'actions', name: i18n.translate('xpack.snapshotRestore.repositoryList.table.actionsColumnTitle', { defaultMessage: 'Actions', }), @@ -302,8 +303,8 @@ export const RepositoryTable: React.FunctionComponent = ({ rowProps={() => ({ 'data-test-subj': 'row', })} - cellProps={() => ({ - 'data-test-subj': 'cell', + cellProps={(item, field) => ({ + 'data-test-subj': `${field.name}_cell`, })} data-test-subj="repositoryTable" /> diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts index 171e949ccee75..b92f21ea6a9b6 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts @@ -11,6 +11,7 @@ import { UIM_REPOSITORY_DELETE, UIM_REPOSITORY_DELETE_MANY, UIM_REPOSITORY_DETAIL_PANEL_VERIFY, + UIM_REPOSITORY_DETAIL_PANEL_CLEANUP, } from '../../constants'; import { uiMetricService } from '../ui_metric'; import { httpService } from './http'; @@ -44,6 +45,20 @@ export const verifyRepository = async (name: Repository['name']) => { return result; }; +export const cleanupRepository = async (name: Repository['name']) => { + const result = await sendRequest({ + path: httpService.addBasePath( + `${API_BASE_PATH}repositories/${encodeURIComponent(name)}/cleanup` + ), + method: 'post', + body: undefined, + }); + + const { trackUiMetric } = uiMetricService; + trackUiMetric(UIM_REPOSITORY_DETAIL_PANEL_CLEANUP); + return result; +}; + export const useLoadRepositoryTypes = () => { return useRequest({ path: httpService.addBasePath(`${API_BASE_PATH}repository_types`), diff --git a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_sr.ts similarity index 73% rename from x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts rename to x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_sr.ts index 82fe30aaa7d2e..794bf99c3d918 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_sr.ts @@ -7,10 +7,10 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { const ca = components.clientAction.factory; - Client.prototype.slm = components.clientAction.namespaceFactory(); - const slm = Client.prototype.slm.prototype; + Client.prototype.sr = components.clientAction.namespaceFactory(); + const sr = Client.prototype.sr.prototype; - slm.policies = ca({ + sr.policies = ca({ urls: [ { fmt: '/_slm/policy', @@ -19,7 +19,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'GET', }); - slm.policy = ca({ + sr.policy = ca({ urls: [ { fmt: '/_slm/policy/<%=name%>', @@ -33,7 +33,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'GET', }); - slm.deletePolicy = ca({ + sr.deletePolicy = ca({ urls: [ { fmt: '/_slm/policy/<%=name%>', @@ -47,7 +47,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'DELETE', }); - slm.executePolicy = ca({ + sr.executePolicy = ca({ urls: [ { fmt: '/_slm/policy/<%=name%>/_execute', @@ -61,7 +61,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'PUT', }); - slm.updatePolicy = ca({ + sr.updatePolicy = ca({ urls: [ { fmt: '/_slm/policy/<%=name%>', @@ -75,7 +75,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'PUT', }); - slm.executeRetention = ca({ + sr.executeRetention = ca({ urls: [ { fmt: '/_slm/_execute_retention', @@ -83,4 +83,18 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'POST', }); + + sr.cleanupRepository = ca({ + urls: [ + { + fmt: '/_snapshot/<%=name%>/_cleanup', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); }; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts index bbfc82b8a6de9..9f434ac10c16a 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts @@ -40,7 +40,7 @@ export const getAllHandler: RouterRouteHandler = async ( // Get policies const policiesByName: { [key: string]: SlmPolicyEs; - } = await callWithRequest('slm.policies', { + } = await callWithRequest('sr.policies', { human: true, }); @@ -62,7 +62,7 @@ export const getOneHandler: RouterRouteHandler = async ( const { name } = req.params; const policiesByName: { [key: string]: SlmPolicyEs; - } = await callWithRequest('slm.policy', { + } = await callWithRequest('sr.policy', { name, human: true, }); @@ -82,7 +82,7 @@ export const getOneHandler: RouterRouteHandler = async ( export const executeHandler: RouterRouteHandler = async (req, callWithRequest) => { const { name } = req.params; - const { snapshot_name: snapshotName } = await callWithRequest('slm.executePolicy', { + const { snapshot_name: snapshotName } = await callWithRequest('sr.executePolicy', { name, }); return { snapshotName }; @@ -98,7 +98,7 @@ export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) => await Promise.all( policyNames.map(name => { - return callWithRequest('slm.deletePolicy', { name }) + return callWithRequest('sr.deletePolicy', { name }) .then(() => response.itemsDeleted.push(name)) .catch(e => response.errors.push({ @@ -122,7 +122,7 @@ export const createHandler: RouterRouteHandler = async (req, callWithRequest) => // Check that policy with the same name doesn't already exist try { - const policyByName = await callWithRequest('slm.policy', { name }); + const policyByName = await callWithRequest('sr.policy', { name }); if (policyByName[name]) { throw conflictError; } @@ -134,7 +134,7 @@ export const createHandler: RouterRouteHandler = async (req, callWithRequest) => } // Otherwise create new policy - return await callWithRequest('slm.updatePolicy', { + return await callWithRequest('sr.updatePolicy', { name, body: serializePolicy(policy), }); @@ -146,10 +146,10 @@ export const updateHandler: RouterRouteHandler = async (req, callWithRequest) => // Check that policy with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callWithRequest('slm.policy', { name }); + await callWithRequest('sr.policy', { name }); // Otherwise update policy - return await callWithRequest('slm.updatePolicy', { + return await callWithRequest('sr.updatePolicy', { name, body: serializePolicy(policy), }); @@ -210,5 +210,5 @@ export const updateRetentionSettingsHandler: RouterRouteHandler = async (req, ca }; export const executeRetentionHandler: RouterRouteHandler = async (_req, callWithRequest) => { - return await callWithRequest('slm.executeRetention'); + return await callWithRequest('sr.executeRetention'); }; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts index f6ac946ab07d5..3d67494da4aad 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -15,6 +15,7 @@ import { RepositoryType, RepositoryVerification, SlmPolicyEs, + RepositoryCleanup, } from '../../../common/types'; import { Plugins } from '../../shim'; @@ -34,6 +35,7 @@ export function registerRepositoriesRoutes(router: Router, plugins: Plugins) { router.get('repositories', getAllHandler); router.get('repositories/{name}', getOneHandler); router.get('repositories/{name}/verify', getVerificationHandler); + router.post('repositories/{name}/cleanup', getCleanupHandler); router.put('repositories', createHandler); router.put('repositories/{name}', updateHandler); router.delete('repositories/{names}', deleteHandler); @@ -74,7 +76,7 @@ export const getAllHandler: RouterRouteHandler = async ( try { const policiesByName: { [key: string]: SlmPolicyEs; - } = await callWithRequest('slm.policies', { + } = await callWithRequest('sr.policies', { human: true, }); const managedRepositoryPolicy = Object.entries(policiesByName) @@ -172,6 +174,31 @@ export const getVerificationHandler: RouterRouteHandler = async ( }; }; +export const getCleanupHandler: RouterRouteHandler = async ( + req, + callWithRequest +): Promise<{ + cleanup: RepositoryCleanup | {}; +}> => { + const { name } = req.params; + + const cleanupResults = await callWithRequest('sr.cleanupRepository', { + name, + }).catch(e => ({ + cleaned: false, + error: e.response ? JSON.parse(e.response) : e, + })); + + return { + cleanup: cleanupResults.error + ? cleanupResults + : { + cleaned: true, + response: cleanupResults, + }, + }; +}; + export const getTypesHandler: RouterRouteHandler = async () => { // In ECE/ESS, do not enable the default types const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES]; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts index 042a2dfeaf6b5..0d34d6a6b1b31 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -38,7 +38,7 @@ export const getAllHandler: RouterRouteHandler = async ( // Attempt to retrieve policies // This could fail if user doesn't have access to read SLM policies try { - const policiesByName = await callWithRequest('slm.policies'); + const policiesByName = await callWithRequest('sr.policies'); policies = Object.keys(policiesByName); } catch (e) { // Silently swallow error as policy names aren't required in UI diff --git a/x-pack/legacy/plugins/snapshot_restore/server/shim.ts b/x-pack/legacy/plugins/snapshot_restore/server/shim.ts index 84c9ddf8e0bea..d64f35c64f11e 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/shim.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/shim.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; import { createRouter, Router } from '../../../server/lib/create_router'; import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; -import { elasticsearchJsPlugin } from './client/elasticsearch_slm'; +import { elasticsearchJsPlugin } from './client/elasticsearch_sr'; import { CloudSetup } from '../../../../plugins/cloud/server'; export interface Core { http: { diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 0083847cfb441..934b44b4accaf 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -49,12 +49,12 @@ export const spaces = (kibana: Record) => uiExports: { styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/spaces/views/management'], + managementSections: [], apps: [ { id: 'space_selector', title: 'Spaces', - main: 'plugins/spaces/views/space_selector', + main: 'plugins/spaces/space_selector', url: 'space_selector', hidden: true, }, diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx new file mode 100644 index 0000000000000..aa3c6acf26236 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx @@ -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 { AdvancedSettingsService } from './advanced_settings_service'; +jest.mock('ui/management', () => { + return { + PAGE_TITLE_COMPONENT: 'page_title_component', + PAGE_SUBTITLE_COMPONENT: 'page_subtitle_component', + }; +}); + +describe('Advanced Settings Service', () => { + describe('#setup', () => { + it('registers space-aware components to augment the advanced settings screen', () => { + const deps = { + getActiveSpace: jest.fn().mockResolvedValue({ id: 'foo', name: 'foo-space' }), + registerSettingsComponent: jest.fn(), + }; + + const advancedSettingsService = new AdvancedSettingsService(); + advancedSettingsService.setup(deps); + + expect(deps.registerSettingsComponent).toHaveBeenCalledTimes(2); + expect(deps.registerSettingsComponent).toHaveBeenCalledWith( + 'page_title_component', + expect.any(Function), + true + ); + + expect(deps.registerSettingsComponent).toHaveBeenCalledWith( + 'page_subtitle_component', + expect.any(Function), + true + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx new file mode 100644 index 0000000000000..9c6c2fcc2cdda --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { PAGE_TITLE_COMPONENT, PAGE_SUBTITLE_COMPONENT } from 'ui/management'; +import { Space } from '../../common/model/space'; +import { AdvancedSettingsTitle, AdvancedSettingsSubtitle } from './components'; + +interface SetupDeps { + getActiveSpace: () => Promise; + registerSettingsComponent: ( + id: string, + component: string | React.FC, + allowOverride: boolean + ) => void; +} + +export class AdvancedSettingsService { + public setup({ getActiveSpace, registerSettingsComponent }: SetupDeps) { + const PageTitle = () => ; + const SubTitle = () => ; + + registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true); + registerSettingsComponent(PAGE_SUBTITLE_COMPONENT, SubTitle, true); + } +} diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx index 433f8a8ccf0a2..e35d67c7214cf 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment, useState, useEffect } from 'react'; -import { Space } from '../../../../../common/model/space'; +import { Space } from '../../../../common/model/space'; interface Props { getActiveSpace: () => Promise; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/index.ts rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx index bf792ca2cdacf..b772ff433abec 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { AdvancedSettingsTitle } from './advanced_settings_title'; -import { SpaceAvatar } from '../../../../components'; +import { SpaceAvatar } from '../../../space_avatar'; import { act } from '@testing-library/react'; describe('AdvancedSettingsTitle', () => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx index af6fa42cce07b..b74524db81d81 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx @@ -7,8 +7,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useEffect } from 'react'; -import { Space } from '../../../../../common/model/space'; -import { SpaceAvatar } from '../../../../components'; +import { Space } from '../../../../../../../plugins/spaces/common/model/space'; +import { SpaceAvatar } from '../../../space_avatar'; interface Props { getActiveSpace: () => Promise; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/index.ts rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts new file mode 100644 index 0000000000000..6678be7fa34e4 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; +export { AdvancedSettingsTitle } from './advanced_settings_title'; diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/index.ts new file mode 100644 index 0000000000000..546831a84fa82 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AdvancedSettingsService } from './advanced_settings_service'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/constants.ts b/x-pack/legacy/plugins/spaces/public/constants.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/constants.ts rename to x-pack/legacy/plugins/spaces/public/constants.ts diff --git a/src/legacy/ui/public/management/_index.scss b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/_index.scss similarity index 100% rename from src/legacy/ui/public/management/_index.scss rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/_index.scss b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_copy_to_space.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/_index.scss rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_copy_to_space.scss diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss new file mode 100644 index 0000000000000..92b19a8c8c6a3 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss @@ -0,0 +1 @@ +@import './copy_to_space'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx index f9da25409d60e..ff9035ff02be5 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx @@ -7,10 +7,7 @@ import React from 'react'; import { EuiLoadingSpinner, EuiText, EuiIconTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - SummarizedCopyToSpaceResult, - SummarizedSavedObjectResult, -} from '../../../../lib/copy_saved_objects_to_space'; +import { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult } from '..'; interface Props { summarizedCopyResult: SummarizedCopyToSpaceResult; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx index 0ad5f72ba3e45..9d73c216c73ce 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { EuiLoadingSpinner, EuiIconTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Space } from '../../../../../common/model/space'; -import { SummarizedCopyToSpaceResult } from '../../../../lib/copy_saved_objects_to_space'; +import { Space } from '../../../common/model/space'; +import { SummarizedCopyToSpaceResult } from '..'; interface Props { space: Space; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index b3fd345b1d2b4..28011911e212e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -6,20 +6,20 @@ import React from 'react'; import Boom from 'boom'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../src/legacy/core_plugins/management/public/np_ready/mocks'; +import { mockManagementPlugin } from '../../../../../../../src/legacy/core_plugins/management/public/np_ready/mocks'; import { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout'; import { CopyToSpaceForm } from './copy_to_space_form'; import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; -import { Space } from '../../../../../common/model/space'; +import { Space } from '../../../common/model/space'; import { findTestSubject } from 'test_utils/find_test_subject'; import { SelectableSpacesControl } from './selectable_spaces_control'; import { act } from '@testing-library/react'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; -import { spacesManagerMock } from '../../../../lib/mocks'; -import { SpacesManager } from '../../../../lib'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -jest.mock('../../../../../../../../../src/legacy/core_plugins/management/public/legacy', () => ({ +jest.mock('../../../../../../../src/legacy/core_plugins/management/public/legacy', () => ({ setup: mockManagementPlugin.createSetupContract(), start: mockManagementPlugin.createStartContract(), })); @@ -404,6 +404,7 @@ describe('CopyToSpaceFlyout', () => { id: 'my-viz', error: { type: 'missing_references', + blocking: [], references: [{ type: 'index-pattern', id: 'missing-index-pattern' }], }, }, diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 5a43e5878ab83..f486f2f24f13d 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -22,17 +22,17 @@ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; import { ProcessedImportResponse, processImportResponse, -} from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { Space } from '../../../../../common/model/space'; -import { SpacesManager } from '../../../../lib'; +} from '../../../../../../../src/legacy/core_plugins/management/public'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; -import { CopyOptions, ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { CopyOptions, ImportRetry } from '../types'; interface Props { onClose: () => void; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx index 5853bebe3c669..56f39ce3ed4fb 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx @@ -8,8 +8,8 @@ import React, { Fragment } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ProcessedImportResponse } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { ImportRetry } from '../types'; interface Props { copyInProgress: boolean; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index 2a7e17c253f0b..f680793e27fe0 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -14,8 +14,8 @@ import { EuiListGroupItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CopyOptions } from '../../../../lib/copy_saved_objects_to_space/types'; -import { Space } from '../../../../../common/model/space'; +import { CopyOptions } from '../types'; +import { Space } from '../../../common/model/space'; import { SelectableSpacesControl } from './selectable_spaces_control'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index b04c9598559b3..285abb828a011 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -13,12 +13,12 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { ProcessedImportResponse } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { summarizeCopyResult } from '../../../../lib/copy_saved_objects_to_space'; -import { Space } from '../../../../../common/model/space'; -import { CopyOptions, ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { Space } from '../../../common/model/space'; +import { CopyOptions, ImportRetry } from '../types'; import { SpaceResult } from './space_result'; +import { summarizeCopyResult } from '..'; interface Props { savedObject: SavedObjectsManagementRecord; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index 42d5707531380..9cf81b1cc4486 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -6,8 +6,8 @@ import React, { Fragment, useState } from 'react'; import { EuiSelectable, EuiLoadingSpinner } from '@elastic/eui'; -import { SpaceAvatar } from '../../../../components'; -import { Space } from '../../../../../common/model/space'; +import { SpaceAvatar } from '../../space_avatar'; +import { Space } from '../../../common/model/space'; interface Props { spaces: Space[]; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx similarity index 86% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx index f71be12276be5..22f0767ba196e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { SummarizedCopyToSpaceResult } from '../../../../lib/copy_saved_objects_to_space'; -import { SpaceAvatar } from '../../../../components'; -import { Space } from '../../../../../common/model/space'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { SummarizedCopyToSpaceResult } from '../index'; +import { SpaceAvatar } from '../../space_avatar'; +import { Space } from '../../../common/model/space'; import { CopyStatusSummaryIndicator } from './copy_status_summary_indicator'; import { SpaceCopyResultDetails } from './space_result_details'; -import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { ImportRetry } from '../types'; interface Props { savedObject: SavedObjectsManagementRecord; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx similarity index 93% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx index 66ec38331c89a..d3ab406b87c3e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx @@ -5,13 +5,13 @@ */ import React from 'react'; -import { SummarizedCopyToSpaceResult } from 'plugins/spaces/lib/copy_saved_objects_to_space'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { Space } from '../../../../../common/model/space'; +import { SummarizedCopyToSpaceResult } from '../index'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { Space } from '../../../common/model/space'; import { CopyStatusIndicator } from './copy_status_indicator'; -import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { ImportRetry } from '../types'; interface Props { savedObject: SavedObjectsManagementRecord; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx similarity index 89% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx index 3b0fffa38e785..c016494a4cdf9 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx @@ -9,8 +9,8 @@ import { toastNotifications } from 'ui/notify'; import { SavedObjectsManagementAction, SavedObjectsManagementRecord, -} from '../../../../../../../src/legacy/core_plugins/management/public'; -import { CopySavedObjectsToSpaceFlyout } from '../../views/management/components/copy_saved_objects_to_space'; +} from '../../../../../../src/legacy/core_plugins/management/public'; +import { CopySavedObjectsToSpaceFlyout } from './components'; import { SpacesManager } from '../spaces_manager'; export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction { diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts new file mode 100644 index 0000000000000..63a59344dfe5d --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementSetup } from 'src/legacy/core_plugins/management/public'; +import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +import { spacesManagerMock } from '../spaces_manager/mocks'; +import { CopySavedObjectsToSpaceService } from '.'; + +describe('CopySavedObjectsToSpaceService', () => { + describe('#setup', () => { + it('registers the CopyToSpaceSavedObjectsManagementAction', () => { + const deps = { + spacesManager: spacesManagerMock.create(), + // we don't have a proper NP mock for this yet + managementSetup: ({ + savedObjects: { + registry: { + has: jest.fn().mockReturnValue(false), + register: jest.fn(), + }, + }, + } as unknown) as ManagementSetup, + }; + + const service = new CopySavedObjectsToSpaceService(); + service.setup(deps); + + expect(deps.managementSetup.savedObjects.registry.register).toHaveBeenCalledTimes(1); + expect(deps.managementSetup.savedObjects.registry.register).toHaveBeenCalledWith( + expect.any(CopyToSpaceSavedObjectsManagementAction) + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts new file mode 100644 index 0000000000000..37354f985a2fc --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementSetup } from 'src/legacy/core_plugins/management/public'; +import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +import { SpacesManager } from '../spaces_manager'; + +interface SetupDeps { + spacesManager: SpacesManager; + managementSetup: ManagementSetup; +} + +export class CopySavedObjectsToSpaceService { + public setup({ spacesManager, managementSetup }: SetupDeps) { + const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager); + managementSetup.savedObjects.registry.register(action); + } +} diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/index.ts similarity index 74% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/index.ts index be23d90cc242a..06969a52a3d8d 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/index.ts @@ -5,4 +5,4 @@ */ export * from './summarize_copy_result'; -export { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +export { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts similarity index 98% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts index 0352902072790..0244a35711e6f 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts @@ -5,7 +5,7 @@ */ import { summarizeCopyResult } from './summarize_copy_result'; -import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; const createSavedObjectsManagementRecord = () => ({ type: 'dashboard', diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts similarity index 96% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts index 8807489157d71..7bc47d35efc6c 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; -import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { SavedObjectsManagementRecord } from '../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; export interface SummarizedSavedObjectResult { type: string; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/types.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/types.ts diff --git a/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts b/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts index 1f41bb89d7707..464066d2221de 100644 --- a/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts +++ b/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts @@ -9,7 +9,7 @@ import { FeatureCatalogueEntry, FeatureCatalogueCategory, } from '../../../../../src/plugins/home/public'; -import { getSpacesFeatureDescription } from './lib/constants'; +import { getSpacesFeatureDescription } from './constants'; export const createSpacesFeatureCatalogueEntry = (): FeatureCatalogueEntry => { return { diff --git a/x-pack/legacy/plugins/spaces/public/index.scss b/x-pack/legacy/plugins/spaces/public/index.scss index 7a40872b760cb..26269f1d31aa3 100644 --- a/x-pack/legacy/plugins/spaces/public/index.scss +++ b/x-pack/legacy/plugins/spaces/public/index.scss @@ -10,4 +10,7 @@ // spcChart__legend--small // spcChart__legend-isLoading -@import './views/index'; +@import './management/index'; +@import './nav_control/index'; +@import './space_selector/index'; +@import './copy_saved_objects_to_space/index'; diff --git a/x-pack/legacy/plugins/spaces/public/index.ts b/x-pack/legacy/plugins/spaces/public/index.ts index 9233aae9fb12f..53cb906a619d3 100644 --- a/x-pack/legacy/plugins/spaces/public/index.ts +++ b/x-pack/legacy/plugins/spaces/public/index.ts @@ -5,6 +5,8 @@ */ import { SpacesPlugin } from './plugin'; +export { SpaceAvatar } from './space_avatar'; + export const plugin = () => { return new SpacesPlugin(); }; diff --git a/x-pack/legacy/plugins/spaces/public/legacy.ts b/x-pack/legacy/plugins/spaces/public/legacy.ts index 99419206093e9..1dffbd2661714 100644 --- a/x-pack/legacy/plugins/spaces/public/legacy.ts +++ b/x-pack/legacy/plugins/spaces/public/legacy.ts @@ -4,15 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { registerSettingsComponent } from 'ui/management'; import { npSetup, npStart } from 'ui/new_platform'; +import { setup as managementSetup } from '../../../../../src/legacy/core_plugins/management/public/legacy'; import { plugin } from '.'; -import { SpacesPlugin, PluginsSetup } from './plugin'; +import { SpacesPlugin, PluginsSetup, PluginsStart } from './plugin'; +import './management/legacy_page_routes'; const spacesPlugin: SpacesPlugin = plugin(); -const plugins: PluginsSetup = { +const pluginsSetup: PluginsSetup = { home: npSetup.plugins.home, + management: managementSetup, + __managementLegacyCompat: { + registerSettingsComponent, + }, }; -export const setup = spacesPlugin.setup(npSetup.core, plugins); -export const start = spacesPlugin.start(npStart.core); +const pluginsStart: PluginsStart = { + management: npStart.plugins.management, +}; + +export const setup = spacesPlugin.setup(npSetup.core, pluginsSetup); +export const start = spacesPlugin.start(npStart.core, pluginsStart); diff --git a/x-pack/legacy/plugins/spaces/public/management/_index.scss b/x-pack/legacy/plugins/spaces/public/management/_index.scss new file mode 100644 index 0000000000000..72deb1f1cde8d --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/_index.scss @@ -0,0 +1,4 @@ +@import './components/confirm_delete_modal/confirm_delete_modal'; +@import './edit_space/enabled_features/index'; +@import './edit_space/section_panel/section_panel'; + diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/_confirm_delete_modal.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/_confirm_delete_modal.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx index f0ab2c99ac2e2..331435b54edb7 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ConfirmDeleteModal } from './confirm_delete_modal'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { spacesManagerMock } from '../../../spaces_manager/mocks'; +import { SpacesManager } from '../../../spaces_manager'; describe('ConfirmDeleteModal', () => { it('renders as expected', () => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx similarity index 98% rename from x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 0c76cb4a828fe..6eed58a784212 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -25,8 +25,8 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; -import { Space } from '../../../../common/model/space'; -import { SpacesManager } from '../../../lib'; +import { SpacesManager } from '../../../spaces_manager'; +import { Space } from '../../../../../../plugins/spaces/common/model/space'; interface Props { space: Space; diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/index.ts new file mode 100644 index 0000000000000..651455d00e9f2 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ConfirmDeleteModal } from './confirm_delete_modal'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/index.ts similarity index 85% rename from x-pack/legacy/plugins/spaces/public/views/management/components/index.ts rename to x-pack/legacy/plugins/spaces/public/management/components/index.ts index 91f4964e1da06..7f9f80f470d12 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/index.ts +++ b/x-pack/legacy/plugins/spaces/public/management/components/index.ts @@ -6,3 +6,4 @@ export { ConfirmDeleteModal } from './confirm_delete_modal'; export { UnauthorizedPrompt } from './unauthorized_prompt'; +export { SecureSpaceMessage } from './secure_space_message'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/index.ts rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.test.tsx similarity index 93% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.test.tsx index 1cc6f6c1f72be..b43010fe5f326 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.test.tsx @@ -8,7 +8,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SecureSpaceMessage } from './secure_space_message'; let mockShowLinks: boolean = true; -jest.mock('../../../../../../xpack_main/public/services/xpack_info', () => { +jest.mock('../../../../../xpack_main/public/services/xpack_info', () => { return { xpackInfo: { get: jest.fn().mockImplementation((key: string) => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx index 6bbc423968b4b..746b7e2ac4c98 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; // @ts-ignore -import { xpackInfo } from '../../../../../../xpack_main/public/services/xpack_info'; +import { xpackInfo } from '../../../../../xpack_main/public/services/xpack_info'; export const SecureSpaceMessage = ({}) => { const showSecurityLinks = xpackInfo.get('features.security.showLinks'); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/index.ts new file mode 100644 index 0000000000000..5a8120a77804b --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UnauthorizedPrompt } from './unauthorized_prompt'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx index 8a7e384d44b35..b0d74afaa90aa 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { isReservedSpace } from '../../../../../common'; -import { Space } from '../../../../../common/model/space'; -import { SpaceAvatar } from '../../../../components'; +import { isReservedSpace } from '../../../../common'; +import { Space } from '../../../../common/model/space'; +import { SpaceAvatar } from '../../../space_avatar'; import { SpaceValidator, toSpaceIdentifier } from '../../lib'; import { SectionPanel } from '../section_panel'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx index 12fa0193b59a4..c3207c82bf95e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx @@ -17,12 +17,10 @@ import { isValidHex, } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; - -import { getSpaceColor, getSpaceInitials } from '../../../../lib/space_attributes'; -import { encode, imageTypes } from '../../../../../common/lib/dataurl'; - -import { MAX_SPACE_INITIALS } from '../../../../../common/constants'; -import { Space } from '../../../../../common/model/space'; +import { imageTypes, encode } from '../../../../common/lib/dataurl'; +import { getSpaceColor, getSpaceInitials } from '../../../space_avatar'; +import { Space } from '../../../../../../../plugins/spaces/common/model/space'; +import { MAX_SPACE_INITIALS } from '../../../../../../../plugins/spaces/common'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.tsx similarity index 98% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.tsx index a717570b19c5d..1d6664273d21e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.tsx @@ -7,7 +7,7 @@ import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { Space } from '../../../../../common/model/space'; +import { Space } from '../../../../common/model/space'; import { SpaceValidator, toSpaceIdentifier } from '../../lib'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx similarity index 88% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx index e7c7dfc5eb1b0..364145d6495b8 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteSpacesButton } from './delete_spaces_button'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; const space = { id: 'my-space', diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx index 216dd7c41f124..56a858eb4ccf6 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx @@ -7,10 +7,9 @@ import { EuiButton, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -// @ts-ignore import { toastNotifications } from 'ui/notify'; -import { Space } from '../../../../common/model/space'; -import { SpacesManager } from '../../../lib/spaces_manager'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/_index.scss b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/_index.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/_index.scss rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx index f8bd4b889394a..f770857d9313d 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx @@ -7,8 +7,8 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { Space } from '../../../../../common/model/space'; +import { Feature } from '../../../../../../../plugins/features/public'; +import { Space } from '../../../../common/model/space'; import { SectionPanel } from '../section_panel'; import { EnabledFeatures } from './enabled_features'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 628be759b7c5c..70312296f757b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment, ReactNode } from 'react'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { Space } from '../../../../../common/model/space'; +import { Feature } from '../../../../../../../plugins/features/public'; +import { Space } from '../../../../common/model/space'; import { getEnabledFeatures } from '../../lib/feature_utils'; import { SectionPanel } from '../section_panel'; import { FeatureTable } from './feature_table'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx similarity index 92% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index b3654b4d35bd3..2866d0bfa8cf3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import { EuiCheckbox, EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui'; + +import { EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component } from 'react'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { Space } from '../../../../../common/model/space'; +import { Feature } from '../../../../../../../plugins/features/public'; +import { Space } from '../../../../common/model/space'; import { ToggleAllFeatures } from './toggle_all_features'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/toggle_all_features.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/toggle_all_features.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index c69a885ae0587..d24e932bce112 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -6,7 +6,7 @@ jest.mock('ui/kfetch', () => ({ kfetch: () => Promise.resolve([{ id: 'feature-1', name: 'feature 1', icon: 'spacesApp' }]), })); -import '../../../__mocks__/xpack_info'; +import '../../__mocks__/xpack_info'; import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; @@ -14,8 +14,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; import { ManageSpacePage } from './manage_space_page'; import { SectionPanel } from './section_panel'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; const space = { id: 'my-space', @@ -65,21 +65,28 @@ describe('ManageSpacePage', () => { }); it('allows a space to be updated', async () => { - const spacesManager = spacesManagerMock.create(); - spacesManager.getSpace = jest.fn().mockResolvedValue({ + const spaceToUpdate = { id: 'existing-space', name: 'Existing Space', description: 'hey an existing space', color: '#aabbcc', initials: 'AB', disabledFeatures: [], + }; + + const spacesManager = spacesManagerMock.create(); + spacesManager.getSpace = jest.fn().mockResolvedValue({ + ...spaceToUpdate, }); spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); + const onLoadSpace = jest.fn(); + const wrapper = mountWithIntl( { await waitForDataLoad(wrapper); expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space'); + expect(onLoadSpace).toHaveBeenCalledWith({ + ...spaceToUpdate, + }); await Promise.resolve(); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.tsx similarity index 93% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.tsx index a5d60d1a731ba..6bbb32ccd654f 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -17,17 +17,15 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { Breadcrumb } from 'ui/chrome'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../../../plugins/features/public'; -import { isReservedSpace } from '../../../../common'; -import { Space } from '../../../../common/model/space'; -import { SpacesManager } from '../../../lib'; -import { SecureSpaceMessage } from '../components/secure_space_message'; -import { UnauthorizedPrompt } from '../components/unauthorized_prompt'; -import { getEditBreadcrumbs, toSpaceIdentifier } from '../lib'; +import { Feature } from '../../../../../../plugins/features/public'; +import { isReservedSpace } from '../../../common'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; +import { SecureSpaceMessage, UnauthorizedPrompt } from '../components'; +import { toSpaceIdentifier } from '../lib'; import { SpaceValidator } from '../lib/validate_space'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; import { CustomizeSpace } from './customize_space'; @@ -39,7 +37,7 @@ interface Props { spacesManager: SpacesManager; spaceId?: string; intl: InjectedIntl; - setBreadcrumbs?: (breadcrumbs: Breadcrumb[]) => void; + onLoadSpace?: (space: Space) => void; capabilities: Capabilities; } @@ -76,7 +74,7 @@ class ManageSpacePageUI extends Component { return; } - const { spaceId, spacesManager, intl, setBreadcrumbs } = this.props; + const { spaceId, spacesManager, intl, onLoadSpace } = this.props; const getFeatures = kfetch({ method: 'get', pathname: '/api/features' }); @@ -84,8 +82,8 @@ class ManageSpacePageUI extends Component { try { const [space, features] = await Promise.all([spacesManager.getSpace(spaceId), getFeatures]); if (space) { - if (setBreadcrumbs) { - setBreadcrumbs(getEditBreadcrumbs(space)); + if (onLoadSpace) { + onLoadSpace(space); } this.setState({ diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx similarity index 89% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx index e4b2dda3a668b..38bf351902096 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { isReservedSpace } from '../../../../common'; -import { Space } from '../../../../common/model/space'; +import { isReservedSpace } from '../../../common'; +import { Space } from '../../../common/model/space'; interface Props { space?: Space; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/_section_panel.scss b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/_section_panel.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/_section_panel.scss rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/_section_panel.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx diff --git a/x-pack/legacy/plugins/spaces/public/management/index.ts b/x-pack/legacy/plugins/spaces/public/management/index.ts new file mode 100644 index 0000000000000..ad3cc6b245619 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ManagementService } from './management_service'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/legacy/plugins/spaces/public/management/legacy_page_routes.tsx similarity index 75% rename from x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx rename to x-pack/legacy/plugins/spaces/public/management/legacy_page_routes.tsx index d8fd0298df2fc..8cf4a129e5b8f 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/legacy_page_routes.tsx @@ -4,30 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ // @ts-ignore -import template from 'plugins/spaces/views/management/template.html'; +import template from 'plugins/spaces/management/template.html'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nContext } from 'ui/i18n'; // @ts-ignore import routes from 'ui/routes'; +import { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; import { npStart } from 'ui/new_platform'; import { ManageSpacePage } from './edit_space'; -import { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './lib'; import { SpacesGridPage } from './spaces_grid'; -import { start as spacesNPStart } from '../../legacy'; +import { start as spacesNPStart } from '../legacy'; +import { Space } from '../../common/model/space'; const reactRootNodeId = 'manageSpacesReactRoot'; +function getListBreadcrumbs() { + return [ + MANAGEMENT_BREADCRUMB, + { + text: 'Spaces', + href: '#/management/spaces/list', + }, + ]; +} + +function getCreateBreadcrumbs() { + return [ + ...getListBreadcrumbs(), + { + text: 'Create', + }, + ]; +} + +function getEditBreadcrumbs(space?: Space) { + return [ + ...getListBreadcrumbs(), + { + text: space ? space.name : '...', + }, + ]; +} + routes.when('/management/spaces/list', { template, k7Breadcrumbs: getListBreadcrumbs, requireUICapability: 'management.kibana.spaces', controller($scope: any) { - $scope.$$postDigest(async () => { + $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); - const { spacesManager } = await spacesNPStart; + const { spacesManager } = spacesNPStart; render( @@ -54,10 +83,10 @@ routes.when('/management/spaces/create', { k7Breadcrumbs: getCreateBreadcrumbs, requireUICapability: 'management.kibana.spaces', controller($scope: any) { - $scope.$$postDigest(async () => { + $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); - const { spacesManager } = await spacesNPStart; + const { spacesManager } = spacesNPStart; render( @@ -100,7 +129,9 @@ routes.when('/management/spaces/edit/:spaceId', { { + npStart.core.chrome.setBreadcrumbs(getEditBreadcrumbs(space)); + }} capabilities={npStart.core.application.capabilities} /> , diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.test.ts similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.test.ts index 8621ec5614368..ce874956d0ef2 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/public'; import { getEnabledFeatures } from './feature_utils'; +import { Feature } from '../../../../../../plugins/features/public'; const buildFeatures = () => [ diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.ts similarity index 74% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.ts index ef46a53967744..ff1688637ef73 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/public'; +import { Feature } from '../../../../../../plugins/features/common'; -import { Space } from '../../../../common/model/space'; +import { Space } from '../../../../../../plugins/spaces/common/model/space'; export function getEnabledFeatures(features: Feature[], space: Partial) { return features.filter(feature => !(space.disabledFeatures || []).includes(feature.id)); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/index.ts b/x-pack/legacy/plugins/spaces/public/management/lib/index.ts similarity index 80% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/index.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/index.ts index f65757f5dba26..4a158168febd8 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/index.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/index.ts @@ -7,5 +7,3 @@ export { toSpaceIdentifier, isValidSpaceIdentifier } from './space_identifier_utils'; export { SpaceValidator } from './validate_space'; - -export { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './breadcrumbs'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.test.ts b/x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.test.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.ts b/x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.test.ts b/x-pack/legacy/plugins/spaces/public/management/lib/validate_space.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.test.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/validate_space.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.ts b/x-pack/legacy/plugins/spaces/public/management/lib/validate_space.ts similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/validate_space.ts index e7b9116131431..43d42dacdd36d 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/validate_space.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { isReservedSpace } from '../../../../common/is_reserved_space'; -import { Space } from '../../../../common/model/space'; +import { isReservedSpace } from '../../../common/is_reserved_space'; +import { Space } from '../../../common/model/space'; import { isValidSpaceIdentifier } from './space_identifier_utils'; interface SpaceValidatorOptions { diff --git a/x-pack/legacy/plugins/spaces/public/management/management_service.test.ts b/x-pack/legacy/plugins/spaces/public/management/management_service.test.ts new file mode 100644 index 0000000000000..fbd39db6969bd --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/management_service.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementService } from '.'; + +const mockSections = { + getSection: jest.fn(), + getAllSections: jest.fn(), + navigateToApp: jest.fn(), +}; + +describe('ManagementService', () => { + describe('#start', () => { + it('registers the spaces management page under the kibana section', () => { + const mockKibanaSection = { + hasItem: jest.fn().mockReturnValue(false), + register: jest.fn(), + }; + + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(mockKibanaSection), + }, + sections: mockSections, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + expect(deps.managementStart.legacy.getSection).toHaveBeenCalledTimes(1); + expect(deps.managementStart.legacy.getSection).toHaveBeenCalledWith('kibana'); + + expect(mockKibanaSection.register).toHaveBeenCalledTimes(1); + expect(mockKibanaSection.register).toHaveBeenCalledWith('spaces', { + name: 'spacesManagementLink', + order: 10, + display: 'Spaces', + url: `#/management/spaces/list`, + }); + }); + + it('will not register the spaces management page twice', () => { + const mockKibanaSection = { + hasItem: jest.fn().mockReturnValue(true), + register: jest.fn(), + }; + + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(mockKibanaSection), + }, + sections: mockSections, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + expect(mockKibanaSection.register).toHaveBeenCalledTimes(0); + }); + + it('will not register the spaces management page if the kibana section is missing', () => { + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(undefined), + }, + sections: mockSections, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + expect(deps.managementStart.legacy.getSection).toHaveBeenCalledTimes(1); + }); + }); + + describe('#stop', () => { + it('deregisters the spaces management page', () => { + const mockKibanaSection = { + hasItem: jest + .fn() + .mockReturnValueOnce(false) + .mockReturnValueOnce(true), + register: jest.fn(), + deregister: jest.fn(), + }; + + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(mockKibanaSection), + }, + sections: mockSections, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + service.stop(); + + expect(mockKibanaSection.register).toHaveBeenCalledTimes(1); + expect(mockKibanaSection.deregister).toHaveBeenCalledTimes(1); + expect(mockKibanaSection.deregister).toHaveBeenCalledWith('spaces'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/public/management/management_service.ts b/x-pack/legacy/plugins/spaces/public/management/management_service.ts new file mode 100644 index 0000000000000..ada38f5cf3387 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/management_service.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { ManagementStart } from 'src/plugins/management/public'; + +interface StartDeps { + managementStart: ManagementStart; +} + +const MANAGE_SPACES_KEY = 'spaces'; + +export class ManagementService { + private kibanaSection!: any; + + public start({ managementStart }: StartDeps) { + this.kibanaSection = managementStart.legacy.getSection('kibana'); + if (this.kibanaSection && !this.kibanaSection.hasItem(MANAGE_SPACES_KEY)) { + this.kibanaSection.register(MANAGE_SPACES_KEY, { + name: 'spacesManagementLink', + order: 10, + display: i18n.translate('xpack.spaces.displayName', { + defaultMessage: 'Spaces', + }), + url: `#/management/spaces/list`, + }); + } + } + + public stop() { + if (this.kibanaSection && this.kibanaSection.hasItem(MANAGE_SPACES_KEY)) { + this.kibanaSection.deregister(MANAGE_SPACES_KEY); + } + } +} diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/index.ts b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/index.ts rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 9fa03b1a9b74a..6ca1877642bdc 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -22,13 +22,13 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../../../plugins/features/public'; -import { isReservedSpace } from '../../../../common'; -import { DEFAULT_SPACE_ID } from '../../../../common/constants'; -import { Space } from '../../../../common/model/space'; -import { SpaceAvatar } from '../../../components'; -import { getSpacesFeatureDescription } from '../../../lib/constants'; -import { SpacesManager } from '../../../lib/spaces_manager'; +import { Feature } from '../../../../../../plugins/features/public'; +import { isReservedSpace } from '../../../common'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { Space } from '../../../common/model/space'; +import { SpaceAvatar } from '../../space_avatar'; +import { getSpacesFeatureDescription } from '../../constants'; +import { SpacesManager } from '../..//spaces_manager'; import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; import { SecureSpaceMessage } from '../components/secure_space_message'; import { UnauthorizedPrompt } from '../components/unauthorized_prompt'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx index 4add607707b24..7856d2e7bee01 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx @@ -6,12 +6,12 @@ jest.mock('ui/kfetch', () => ({ kfetch: () => Promise.resolve([]), })); -import '../../../__mocks__/xpack_info'; +import '../../__mocks__/xpack_info'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { SpaceAvatar } from '../../../components'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { SpaceAvatar } from '../../space_avatar'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; import { SpacesGridPage } from './spaces_grid_page'; const spaces = [ diff --git a/x-pack/legacy/plugins/spaces/public/views/management/template.html b/x-pack/legacy/plugins/spaces/public/management/template.html similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/template.html rename to x-pack/legacy/plugins/spaces/public/management/template.html diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/_index.scss b/x-pack/legacy/plugins/spaces/public/nav_control/_index.scss similarity index 55% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/_index.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/_index.scss index 192091fb04e3c..d0471da325cec 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/_index.scss +++ b/x-pack/legacy/plugins/spaces/public/nav_control/_index.scss @@ -1 +1,2 @@ @import './components/index'; +@import './nav_control'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/_nav_control.scss b/x-pack/legacy/plugins/spaces/public/nav_control/_nav_control.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/_nav_control.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/_nav_control.scss diff --git a/x-pack/legacy/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/manage_spaces_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/manage_spaces_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/spaces_description.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/spaces_description.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/_index.scss b/x-pack/legacy/plugins/spaces/public/nav_control/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/_index.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/components/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_description.scss b/x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_description.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_description.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_description.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_menu.scss b/x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_menu.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_menu.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_menu.scss diff --git a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx index 91a0803c20bc9..857d0c1f828a6 100644 --- a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx @@ -8,7 +8,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, CSSProperties } from 'react'; import { Capabilities } from 'src/core/public'; -import { getManageSpacesUrl } from '../lib/constants'; +import { getManageSpacesUrl } from '../../constants'; interface Props { isDisabled?: boolean; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.tsx similarity index 89% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.tsx index 043fc656a571e..abf3c636b839e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.tsx @@ -7,8 +7,8 @@ import { EuiContextMenuPanel, EuiText } from '@elastic/eui'; import React, { FC } from 'react'; import { Capabilities } from 'src/core/public'; -import { ManageSpacesButton } from '../../../components'; -import { getSpacesFeatureDescription } from '../../../lib/constants'; +import { ManageSpacesButton } from './manage_spaces_button'; +import { getSpacesFeatureDescription } from '../../constants'; interface Props { onManageSpacesClick: () => void; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_menu.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 9a26f6802abdf..96ce18896b426 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -14,9 +14,10 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; import { Capabilities } from 'src/core/public'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants'; -import { Space } from '../../../../common/model/space'; -import { ManageSpacesButton, SpaceAvatar } from '../../../components'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common/constants'; +import { Space } from '../../../common/model/space'; +import { ManageSpacesButton } from './manage_spaces_button'; +import { SpaceAvatar } from '../../space_avatar'; interface Props { spaces: Space[]; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts b/x-pack/legacy/plugins/spaces/public/nav_control/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts rename to x-pack/legacy/plugins/spaces/public/nav_control/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/nav_control.tsx index 0df077e0d2da0..9ec070eff3fed 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SpacesManager } from 'plugins/spaces/lib/spaces_manager'; import React from 'react'; import ReactDOM from 'react-dom'; import { CoreStart } from 'src/core/public'; +import { SpacesManager } from '../spaces_manager'; import { NavControlPopover } from './nav_control_popover'; export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreStart) { diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.test.tsx similarity index 91% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.test.tsx index a04f28242f984..5ce141abb713e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.test.tsx @@ -7,9 +7,9 @@ import * as Rx from 'rxjs'; import { shallow } from 'enzyme'; import React from 'react'; -import { SpaceAvatar } from '../../components'; -import { spacesManagerMock } from '../../lib/mocks'; -import { SpacesManager } from '../../lib'; +import { SpaceAvatar } from '../space_avatar'; +import { spacesManagerMock } from '../spaces_manager/mocks'; +import { SpacesManager } from '../spaces_manager'; import { NavControlPopover } from './nav_control_popover'; import { EuiHeaderSectionItemButton } from '@elastic/eui'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -42,6 +42,7 @@ describe('NavControlPopover', () => { disabledFeatures: [], }, ]); + // @ts-ignore readonly check spacesManager.onActiveSpaceChange$ = Rx.of({ id: 'foo-space', name: 'foo', diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.tsx index b37458aace2a2..f291027e15232 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.tsx @@ -13,9 +13,9 @@ import { import React, { Component } from 'react'; import { Capabilities } from 'src/core/public'; import { Subscription } from 'rxjs'; -import { Space } from '../../../common/model/space'; -import { SpaceAvatar } from '../../components'; -import { SpacesManager } from '../../lib/spaces_manager'; +import { Space } from '../../common/model/space'; +import { SpaceAvatar } from '../space_avatar'; +import { SpacesManager } from '../spaces_manager'; import { SpacesDescription } from './components/spaces_description'; import { SpacesMenu } from './components/spaces_menu'; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/types.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/types.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/types.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/types.tsx diff --git a/x-pack/legacy/plugins/spaces/public/plugin.tsx b/x-pack/legacy/plugins/spaces/public/plugin.tsx index 4e070c3cee3df..1ddb69a5b595c 100644 --- a/x-pack/legacy/plugins/spaces/public/plugin.tsx +++ b/x-pack/legacy/plugins/spaces/public/plugin.tsx @@ -6,9 +6,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; -import { SpacesManager } from './lib'; -import { initSpacesNavControl } from './views/nav_control'; +import { ManagementSetup } from 'src/legacy/core_plugins/management/public'; +import { ManagementStart } from 'src/plugins/management/public'; +import { SpacesManager } from './spaces_manager'; +import { initSpacesNavControl } from './nav_control'; import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; +import { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space'; +import { AdvancedSettingsService } from './advanced_settings'; +import { ManagementService } from './management'; export interface SpacesPluginStart { spacesManager: SpacesManager | null; @@ -16,25 +21,61 @@ export interface SpacesPluginStart { export interface PluginsSetup { home?: HomePublicPluginSetup; + management: ManagementSetup; + __managementLegacyCompat: { + registerSettingsComponent: ( + id: string, + component: string | React.FC, + allowOverride: boolean + ) => void; + }; +} + +export interface PluginsStart { + management: ManagementStart; } export class SpacesPlugin implements Plugin { - private spacesManager: SpacesManager | null = null; + private spacesManager!: SpacesManager; - public async start(core: CoreStart) { - const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string; + private managementService?: ManagementService; + public setup(core: CoreSetup, plugins: PluginsSetup) { + const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string; this.spacesManager = new SpacesManager(serverBasePath, core.http); + + const copySavedObjectsToSpaceService = new CopySavedObjectsToSpaceService(); + copySavedObjectsToSpaceService.setup({ + spacesManager: this.spacesManager, + managementSetup: plugins.management, + }); + + const advancedSettingsService = new AdvancedSettingsService(); + advancedSettingsService.setup({ + getActiveSpace: () => this.spacesManager.getActiveSpace(), + registerSettingsComponent: plugins.__managementLegacyCompat.registerSettingsComponent, + }); + + if (plugins.home) { + plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); + } + } + + public start(core: CoreStart, plugins: PluginsStart) { initSpacesNavControl(this.spacesManager, core); + this.managementService = new ManagementService(); + this.managementService.start({ managementStart: plugins.management }); + return { spacesManager: this.spacesManager, }; } - public async setup(core: CoreSetup, plugins: PluginsSetup) { - if (plugins.home) { - plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); + public stop() { + if (this.managementService) { + this.managementService.stop(); + this.managementService = undefined; } } } diff --git a/x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/space_avatar/__snapshots__/space_avatar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/space_avatar/__snapshots__/space_avatar.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/components/index.ts b/x-pack/legacy/plugins/spaces/public/space_avatar/index.ts similarity index 82% rename from x-pack/legacy/plugins/spaces/public/components/index.ts rename to x-pack/legacy/plugins/spaces/public/space_avatar/index.ts index 2e73f0c704f8c..1525f2c8c6186 100644 --- a/x-pack/legacy/plugins/spaces/public/components/index.ts +++ b/x-pack/legacy/plugins/spaces/public/space_avatar/index.ts @@ -5,4 +5,4 @@ */ export { SpaceAvatar } from './space_avatar'; -export { ManageSpacesButton } from './manage_spaces_button'; +export * from './space_attributes'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts b/x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts b/x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.ts diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.test.tsx b/x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/space_avatar.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.tsx similarity index 98% rename from x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.tsx index 0d9751ca43db9..c89f492a8fc99 100644 --- a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.tsx @@ -8,7 +8,7 @@ import { EuiAvatar, isValidHex } from '@elastic/eui'; import React, { FC } from 'react'; import { MAX_SPACE_INITIALS } from '../../common'; import { Space } from '../../common/model/space'; -import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from '../lib/space_attributes'; +import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from './space_attributes'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/space_selector/_index.scss b/x-pack/legacy/plugins/spaces/public/space_selector/_index.scss new file mode 100644 index 0000000000000..0621aa2a3efd7 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/space_selector/_index.scss @@ -0,0 +1,2 @@ +@import './space_selector'; +@import './components/index'; diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss b/x-pack/legacy/plugins/spaces/public/space_selector/_space_selector.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/_space_selector.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/_index.scss b/x-pack/legacy/plugins/spaces/public/space_selector/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/_index.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/components/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/_space_card.scss b/x-pack/legacy/plugins/spaces/public/space_selector/components/_space_card.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/_space_card.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/components/_space_card.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/_space_cards.scss b/x-pack/legacy/plugins/spaces/public/space_selector/components/_space_cards.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/_space_cards.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/components/_space_cards.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/index.ts b/x-pack/legacy/plugins/spaces/public/space_selector/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/index.ts rename to x-pack/legacy/plugins/spaces/public/space_selector/components/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_card.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/components/space_card.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.tsx index 2386f6a6fe9d0..f898ba87c60bd 100644 --- a/x-pack/legacy/plugins/spaces/public/views/components/space_card.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.tsx @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - // FIXME: need updated typedefs - // @ts-ignore - EuiCard, -} from '@elastic/eui'; +import { EuiCard } from '@elastic/eui'; import React from 'react'; import { Space } from '../../../common/model/space'; -import { SpaceAvatar } from '../../components'; +import { SpaceAvatar } from '../../space_avatar'; interface Props { space: Space; diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_cards.test.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/space_cards.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_cards.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/space_cards.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/index.tsx similarity index 82% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/index.tsx index c520c2683c965..c1c1b6dc3a2f3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/index.tsx @@ -5,7 +5,7 @@ */ // @ts-ignore -import template from 'plugins/spaces/views/space_selector/space_selector.html'; +import template from 'plugins/spaces/space_selector/space_selector.html'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; // @ts-ignore @@ -15,14 +15,14 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { SpaceSelector } from './space_selector'; -import { start as spacesNPStart } from '../../legacy'; +import { start as spacesNPStart } from '../legacy'; const module = uiModules.get('spaces_selector', []); module.controller('spacesSelectorController', ($scope: any) => { - $scope.$$postDigest(async () => { + $scope.$$postDigest(() => { const domNode = document.getElementById('spaceSelectorRoot'); - const { spacesManager } = await spacesNPStart; + const { spacesManager } = spacesNPStart; render( diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.html b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.html similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.html rename to x-pack/legacy/plugins/spaces/public/space_selector/space_selector.html diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.test.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.test.tsx similarity index 92% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/space_selector.test.tsx index 829312061ca98..b4d0f96307500 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../common/model/space'; -import { spacesManagerMock } from '../../lib/mocks'; +import { Space } from '../../common/model/space'; +import { spacesManagerMock } from '../spaces_manager/mocks'; import { SpaceSelector } from './space_selector'; function getSpacesManager(spaces: Space[] = []) { diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/space_selector.tsx index d665752b3c8a6..206d38454fa8c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.tsx @@ -19,11 +19,11 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { SpacesManager } from 'plugins/spaces/lib'; import React, { Component, Fragment } from 'react'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common/constants'; -import { Space } from '../../../common/model/space'; -import { SpaceCards } from '../components/space_cards'; +import { SpacesManager } from '../spaces_manager'; +import { Space } from '../../common/model/space'; +import { SpaceCards } from './components'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../common/constants'; interface Props { spacesManager: SpacesManager; diff --git a/x-pack/legacy/plugins/spaces/public/lib/index.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/index.ts similarity index 76% rename from x-pack/legacy/plugins/spaces/public/lib/index.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/index.ts index 56ac7b8ff37f4..538dd77e053f5 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/index.ts +++ b/x-pack/legacy/plugins/spaces/public/spaces_manager/index.ts @@ -5,4 +5,3 @@ */ export { SpacesManager } from './spaces_manager'; -export { getSpaceInitials, getSpaceColor, getSpaceImageUrl } from './space_attributes'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/mocks.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/mocks.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/mocks.ts diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts similarity index 87% rename from x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 69c6f7a452fdd..56879af33916f 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts +++ b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -6,9 +6,10 @@ import { of, Observable } from 'rxjs'; import { Space } from '../../common/model/space'; +import { SpacesManager } from './spaces_manager'; function createSpacesManagerMock() { - return { + return ({ onActiveSpaceChange$: (of(undefined) as unknown) as Observable, getSpaces: jest.fn().mockResolvedValue([]), getSpace: jest.fn().mockResolvedValue(undefined), @@ -19,7 +20,8 @@ function createSpacesManagerMock() { copySavedObjects: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), - }; + changeSelectedSpace: jest.fn(), + } as unknown) as jest.Mocked; } export const spacesManagerMock = { diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.test.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/spaces_manager.test.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.ts similarity index 97% rename from x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.ts index ccc1b00dabb29..e9c738cf40c69 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -9,9 +9,9 @@ import { HttpSetup } from 'src/core/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/legacy/core_plugins/management/public'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; -import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/types'; import { ENTER_SPACE_PATH } from '../../common/constants'; import { addSpaceIdToPath } from '../../../../../plugins/spaces/common'; +import { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; export class SpacesManager { private activeSpace$: BehaviorSubject = new BehaviorSubject(null); diff --git a/x-pack/legacy/plugins/spaces/public/views/_index.scss b/x-pack/legacy/plugins/spaces/public/views/_index.scss deleted file mode 100644 index 0cc8ccb10246b..0000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './components/index'; -@import './management/index'; -@import './nav_control/index'; -@import './space_selector/index' diff --git a/x-pack/legacy/plugins/spaces/public/views/management/_index.scss b/x-pack/legacy/plugins/spaces/public/views/management/_index.scss deleted file mode 100644 index e7cbdfe2de7e8..0000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './components/confirm_delete_modal'; -@import './edit_space/enabled_features/index'; -@import './components/copy_saved_objects_to_space/index'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx deleted file mode 100644 index bf33273c614d6..0000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/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 { i18n } from '@kbn/i18n'; -import 'plugins/spaces/views/management/page_routes'; -import React from 'react'; -import { - management, - PAGE_SUBTITLE_COMPONENT, - PAGE_TITLE_COMPONENT, - registerSettingsComponent, -} from 'ui/management'; -// @ts-ignore -import routes from 'ui/routes'; -import { setup as managementSetup } from '../../../../../../../src/legacy/core_plugins/management/public/legacy'; -import { AdvancedSettingsSubtitle } from './components/advanced_settings_subtitle'; -import { AdvancedSettingsTitle } from './components/advanced_settings_title'; -import { start as spacesNPStart } from '../../legacy'; -import { CopyToSpaceSavedObjectsManagementAction } from '../../lib/copy_saved_objects_to_space'; - -const MANAGE_SPACES_KEY = 'spaces'; - -routes.defaults(/\/management/, { - resolve: { - spacesManagementSection() { - function getKibanaSection() { - return management.getSection('kibana'); - } - - function deregisterSpaces() { - getKibanaSection().deregister(MANAGE_SPACES_KEY); - } - - function ensureSpagesRegistered() { - const kibanaSection = getKibanaSection(); - - if (!kibanaSection.hasItem(MANAGE_SPACES_KEY)) { - kibanaSection.register(MANAGE_SPACES_KEY, { - name: 'spacesManagementLink', - order: 10, - display: i18n.translate('xpack.spaces.displayName', { - defaultMessage: 'Spaces', - }), - url: `#/management/spaces/list`, - }); - } - - // Customize Saved Objects Management - spacesNPStart.then(({ spacesManager }) => { - const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager!); - // This route resolve function executes any time the management screen is loaded, and we want to ensure - // that this action is only registered once. - if (!managementSetup.savedObjects.registry.has(action.id)) { - managementSetup.savedObjects.registry.register(action); - } - }); - - const getActiveSpace = async () => { - const { spacesManager } = await spacesNPStart; - return spacesManager!.getActiveSpace(); - }; - - const PageTitle = () => ; - registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true); - - const SubTitle = () => ; - registerSettingsComponent(PAGE_SUBTITLE_COMPONENT, SubTitle, true); - } - - deregisterSpaces(); - - ensureSpagesRegistered(); - }, - }, -}); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts b/x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts deleted file mode 100644 index a4e8ba508b617..0000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts +++ /dev/null @@ -1,36 +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 { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; -import { Space } from '../../../../common/model/space'; - -export function getListBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: 'Spaces', - href: '#/management/spaces/list', - }, - ]; -} - -export function getCreateBreadcrumbs() { - return [ - ...getListBreadcrumbs(), - { - text: 'Create', - }, - ]; -} - -export function getEditBreadcrumbs(space?: Space) { - return [ - ...getListBreadcrumbs(), - { - text: space ? space.name : '...', - }, - ]; -} diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss b/x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss deleted file mode 100644 index f23ac662dce1d..0000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './space_selector'; diff --git a/x-pack/legacy/plugins/transform/public/shim.ts b/x-pack/legacy/plugins/transform/public/shim.ts index f8edc752c9a21..d739dd2edddcc 100644 --- a/x-pack/legacy/plugins/transform/public/shim.ts +++ b/x-pack/legacy/plugins/transform/public/shim.ts @@ -47,6 +47,7 @@ export interface Core extends npCore { esDocBasePath: string; esPluginDocBasePath: string; esStackOverviewDocBasePath: string; + esMLDocBasePath: string; }; docTitle: { change: typeof docTitle.change; @@ -93,6 +94,7 @@ export function createPublicShim(): { core: Core; plugins: Plugins } { esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`, esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`, esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`, + esMLDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, }, docTitle: { change: docTitle.change, diff --git a/x-pack/legacy/plugins/uptime/README.md b/x-pack/legacy/plugins/uptime/README.md index 37e030c44db37..308f78ecdc368 100644 --- a/x-pack/legacy/plugins/uptime/README.md +++ b/x-pack/legacy/plugins/uptime/README.md @@ -3,7 +3,7 @@ ## Purpose The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening -in their infrasturcture. It's primarily built using React and Apollo's GraphQL tools. +in their infrastructure. It's primarily built using React and Apollo's GraphQL tools. ## Layout @@ -44,7 +44,7 @@ From `~/kibana/x-pack`, run `node scripts/jest.js`. ### Functional tests In one shell, from **~/kibana/x-pack**: -`node scripts/functional_tests-server.js` +`node scripts/functional_tests_server.js` In another shell, from **~kibana/x-pack**: `node ../scripts/functional_test_runner.js --grep="{TEST_NAME}"`. diff --git a/x-pack/legacy/plugins/uptime/common/graphql/introspection.json b/x-pack/legacy/plugins/uptime/common/graphql/introspection.json index 4d1993233e9ca..19d9cf19cc7f8 100644 --- a/x-pack/legacy/plugins/uptime/common/graphql/introspection.json +++ b/x-pack/legacy/plugins/uptime/common/graphql/introspection.json @@ -352,25 +352,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "getMonitorPageTitle", - "description": "", - "args": [ - { - "name": "monitorId", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "MonitorPageTitle", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "getMonitorStates", "description": "Fetches the current state of Uptime monitors for the given parameters.", @@ -2173,18 +2154,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "mixed", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "total", "description": "", @@ -2561,45 +2530,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "MonitorPageTitle", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "MonitorSummaryResult", diff --git a/x-pack/legacy/plugins/uptime/common/graphql/types.ts b/x-pack/legacy/plugins/uptime/common/graphql/types.ts index ed7c9ef19f484..92e27d20323a7 100644 --- a/x-pack/legacy/plugins/uptime/common/graphql/types.ts +++ b/x-pack/legacy/plugins/uptime/common/graphql/types.ts @@ -30,9 +30,6 @@ export interface Query { /** Fetch the most recent event data for a monitor ID, date range, location. */ getLatestMonitors: Ping[]; - getFilterBar?: FilterBar | null; - - getMonitorPageTitle?: MonitorPageTitle | null; /** Fetches the current state of Uptime monitors for the given parameters. */ getMonitorStates?: MonitorSummaryResult | null; /** Fetches details about the uptime index. */ @@ -419,8 +416,6 @@ export interface SnapshotCount { down: number; - mixed: number; - total: number; } @@ -470,29 +465,7 @@ export interface StatusData { /** The total down counts for this point. */ total?: number | null; } -/** The data used to enrich the filter bar. */ -export interface FilterBar { - /** A series of monitor IDs in the heartbeat indices. */ - ids?: string[] | null; - /** The location values users have configured for the agents. */ - locations?: string[] | null; - /** The ports of the monitored endpoints. */ - ports?: number[] | null; - /** The schemes used by the monitors. */ - schemes?: string[] | null; - /** The possible status values contained in the indices. */ - statuses?: string[] | null; - /** The list of URLs */ - urls?: string[] | null; -} - -export interface MonitorPageTitle { - id: string; - - url?: string | null; - name?: string | null; -} /** The primary object returned for monitor states. */ export interface MonitorSummaryResult { /** Used to go to the next page of results */ @@ -738,24 +711,12 @@ export interface GetMonitorChartsDataQueryArgs { location?: string | null; } -export interface GetLatestMonitorsQueryArgs { - /** The lower limit of the date range. */ - dateRangeStart: string; - /** The upper limit of the date range. */ - dateRangeEnd: string; - /** Optional: a specific monitor ID filter. */ - monitorId?: string | null; - /** Optional: a specific instance location filter. */ - location?: string | null; -} export interface GetFilterBarQueryArgs { dateRangeStart: string; dateRangeEnd: string; } -export interface GetMonitorPageTitleQueryArgs { - monitorId: string; -} + export interface GetMonitorStatesQueryArgs { dateRangeStart: string; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts index 224892eb91783..58f79abcf91ec 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -5,6 +5,6 @@ */ export * from './common'; +export * from './monitor'; +export * from './overview_filters'; export * from './snapshot'; -export * from './monitor/monitor_details'; -export * from './monitor/monitor_locations'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts similarity index 99% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts rename to x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts index 0a57a67b898e0..bf81c91bae633 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import * as t from 'io-ts'; // IO type for validation diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts new file mode 100644 index 0000000000000..80b48d09dc5b8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './details'; +export * from './locations'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_locations.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_locations.ts rename to x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts new file mode 100644 index 0000000000000..a803a0720959a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { OverviewFiltersType, OverviewFilters } from './overview_filters'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts new file mode 100644 index 0000000000000..9b9241494f001 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const OverviewFiltersType = t.type({ + locations: t.array(t.string), + ports: t.array(t.number), + schemes: t.array(t.string), + tags: t.array(t.string), +}); + +export type OverviewFilters = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts index d4935c50ff5b8..3abc25530a2dc 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts @@ -8,7 +8,6 @@ import * as t from 'io-ts'; export const SnapshotType = t.type({ down: t.number, - mixed: t.number, total: t.number, up: t.number, }); diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts index bc4e30b79cb15..c09fdf116e790 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts @@ -30,14 +30,8 @@ export class Plugin { } public start(start: StartObject): void { - const { - core, - plugins: { - data: { autocomplete }, - }, - } = start; const libs: UMFrontendLibs = { - framework: getKibanaFrameworkAdapter(core, autocomplete), + framework: getKibanaFrameworkAdapter(start.core, start.plugins), }; // @ts-ignore improper type description this.chrome.setRootTemplate(template); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/empty_status_bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/empty_status_bar.test.tsx.snap deleted file mode 100644 index e18846e960122..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/empty_status_bar.test.tsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmptyStatusBar component renders a default message when no message provided 1`] = ` - - - - No data found for monitor id mon_id - - - -`; - -exports[`EmptyStatusBar component renders a message when provided 1`] = ` - - - - foobarbaz - - - -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap index 45c24fd11194d..d731a168225b7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap @@ -5,22 +5,14 @@ Array [
, - .c0 { - margin-left: 20px; -} - -
-
- SSL certificate expires in 2 months -
+ SSL certificate expires in 2 months
, ] diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap index e53235b1ed6f7..17588ae53ed00 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap @@ -2,46 +2,24 @@ exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
-
-
- -
-
- Up -
-
+

+ Up in 2 Locations +

- 1234ms -
-
- 15 minutes ago + +

+ id1 +

+
+
+
`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap index a38ca85a3e9e7..9a4cb2e04f59b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap @@ -227,54 +227,24 @@ exports[`UptimeDatePicker component validates props with shallow render 1`] = ` commonlyUsedRanges={ Array [ Object { - "end": "now/d", + "end": "now", "label": "Today", "start": "now/d", }, - Object { - "end": "now/w", - "label": "This week", - "start": "now/w", - }, - Object { - "end": "now", - "label": "Last 15 minutes", - "start": "now-15m", - }, - Object { - "end": "now", - "label": "Last 30 minutes", - "start": "now-30m", - }, Object { "end": "now", - "label": "Last 1 hour", - "start": "now-1h", - }, - Object { - "end": "now", - "label": "Last 24 hours", - "start": "now-24h", - }, - Object { - "end": "now", - "label": "Last 7 days", - "start": "now-7d", - }, - Object { - "end": "now", - "label": "Last 30 days", - "start": "now-30d", + "label": "Week to date", + "start": "now/w", }, Object { "end": "now", - "label": "Last 90 days", - "start": "now-90d", + "label": "Month to date", + "start": "now/M", }, Object { "end": "now", - "label": "Last 2 year", - "start": "now-1y", + "label": "Year to date", + "start": "now/y", }, ] } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/empty_status_bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/empty_status_bar.test.tsx deleted file mode 100644 index b815f0e38b8e2..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/empty_status_bar.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { EmptyStatusBar } from '../empty_status_bar'; - -describe('EmptyStatusBar component', () => { - it('renders a message when provided', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('renders a default message when no message provided', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_ssl_certificate.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_ssl_certificate.test.tsx index c4063208d046e..03eb252aa8c09 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_ssl_certificate.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_ssl_certificate.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import moment from 'moment'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { PingTls } from '../../../../common/graphql/types'; -import { MonitorSSLCertificate } from '../monitor_status_bar'; +import { MonitorSSLCertificate } from '../monitor_status_details/monitor_status_bar'; describe('MonitorStatusBar component', () => { let monitorTls: PingTls; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx index 761dd8a65238f..545405f91d537 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx @@ -8,34 +8,59 @@ import moment from 'moment'; import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { Ping } from '../../../../common/graphql/types'; -import { MonitorStatusBarComponent } from '../monitor_status_bar'; +import { MonitorStatusBarComponent } from '../monitor_status_details/monitor_status_bar'; describe('MonitorStatusBar component', () => { - let monitorStatus: Ping[]; + let monitorStatus: Ping; + let monitorLocations: any; + let dateStart: string; + let dateEnd: string; beforeEach(() => { - monitorStatus = [ - { - id: 'id1', - timestamp: moment(new Date()) - .subtract(15, 'm') - .toString(), - monitor: { - duration: { - us: 1234567, - }, - status: 'up', - }, - url: { - full: 'https://www.example.com/', + monitorStatus = { + id: 'id1', + timestamp: moment(new Date()) + .subtract(15, 'm') + .toString(), + monitor: { + duration: { + us: 1234567, }, + status: 'up', + }, + url: { + full: 'https://www.example.com/', }, - ]; + }; + + monitorLocations = { + monitorId: 'secure-avc', + locations: [ + { + summary: { up: 4, down: 0 }, + geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + { + summary: { up: 4, down: 0 }, + geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + ], + }; + + dateStart = moment('01-01-2010').toString(); + dateEnd = moment('10-10-2010').toString(); }); it('renders duration in ms, not us', () => { const component = renderWithIntl( - + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index 193f37c8fe56b..d645eb21ac776 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -13,7 +13,6 @@ describe('Snapshot component', () => { const snapshot: Snapshot = { up: 8, down: 2, - mixed: 0, total: 10, }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx index 93fa0b505a891..e3ca1a87850c8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx @@ -6,37 +6,16 @@ import { shallowWithIntl, renderWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; -import { UptimeDatePicker, CommonlyUsedRange } from '../uptime_date_picker'; +import { UptimeDatePicker } from '../uptime_date_picker'; describe('UptimeDatePicker component', () => { - let commonlyUsedRange: CommonlyUsedRange[]; - - beforeEach(() => { - commonlyUsedRange = [ - { from: 'now/d', to: 'now/d', display: 'Today' }, - { from: 'now/w', to: 'now/w', display: 'This week' }, - { from: 'now-15m', to: 'now', display: 'Last 15 minutes' }, - { from: 'now-30m', to: 'now', display: 'Last 30 minutes' }, - { from: 'now-1h', to: 'now', display: 'Last 1 hour' }, - { from: 'now-24h', to: 'now', display: 'Last 24 hours' }, - { from: 'now-7d', to: 'now', display: 'Last 7 days' }, - { from: 'now-30d', to: 'now', display: 'Last 30 days' }, - { from: 'now-90d', to: 'now', display: 'Last 90 days' }, - { from: 'now-1y', to: 'now', display: 'Last 2 year' }, - ]; - }); - it('validates props with shallow render', () => { - const component = shallowWithIntl( - - ); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('renders properly with mock data', () => { - const component = renderWithIntl( - - ); + const component = renderWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap index c8c4177a4907e..9699a1842ccf1 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap @@ -103,6 +103,7 @@ exports[`DonutChart component renders a donut chart 1`] = ` 32 @@ -150,6 +151,7 @@ exports[`DonutChart component renders a donut chart 1`] = ` 95 diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap index 3674497b538f3..e971576521b7d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap @@ -5,6 +5,7 @@ exports[`DonutChartLegend applies valid props as expected 1`] = ` diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap index 5f6508e299a28..bc6033ea7109a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap @@ -21,6 +21,7 @@ exports[`DonutChartLegendRow passes appropriate props 1`] = ` 23 diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend_row.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend_row.test.tsx index 82e2279edd892..49e887cc8f96c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend_row.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend_row.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; describe('DonutChartLegendRow', () => { it('passes appropriate props', () => { const wrapper = shallowWithIntl( - + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx index e2705e7cbacb3..50dca8577455d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx @@ -73,7 +73,7 @@ export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { { ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend_row.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend_row.tsx index bf684a3446b9a..fc67a86db3b48 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend_row.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend_row.tsx @@ -23,9 +23,10 @@ interface Props { color: string; message: string; content: string | number; + 'data-test-subj': string; } -export const DonutChartLegendRow = ({ color, content, message }: Props) => ( +export const DonutChartLegendRow = ({ color, content, message, 'data-test-subj': dts }: Props) => ( @@ -33,6 +34,8 @@ export const DonutChartLegendRow = ({ color, content, message }: Props) => ( {message} - {content} + + {content} + ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx index b5dcfce032724..775e8c0c06aa5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx @@ -66,32 +66,32 @@ export const DurationChart = ({ - - - - getTickFormat(d)} - title={i18n.translate('xpack.uptime.monitorCharts.durationChart.leftAxis.title', { - defaultMessage: 'Duration ms', - })} - /> - {hasLines ? ( + {hasLines ? ( + + + + getTickFormat(d)} + title={i18n.translate('xpack.uptime.monitorCharts.durationChart.leftAxis.title', { + defaultMessage: 'Duration ms', + })} + /> - ) : ( - - )} - + + ) : ( + + )} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap index 4dfc837c29b55..36c54758cf116 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap @@ -20,7 +20,7 @@ exports[`DataMissing component renders basePath and headingMessage 1`] = ` values={ Object { "configureHeartbeatLink": @@ -301,7 +299,6 @@ exports[`EmptyState component does not render empty state with appropriate base exports[`EmptyState component doesn't render child components when count is falsey 1`] = ` @@ -783,7 +778,6 @@ exports[`EmptyState component renders child components when count is truthy 1`] exports[`EmptyState component renders error message when an error occurs 1`] = ` { it('renders basePath and headingMessage', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx index 0077292d91a46..32d55519acf27 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx @@ -24,7 +24,7 @@ describe('EmptyState component', () => { it('renders child components when count is truthy', () => { const component = shallowWithIntl( - +
Foo
Bar
Baz
@@ -35,7 +35,7 @@ describe('EmptyState component', () => { it(`doesn't render child components when count is falsey`, () => { const component = mountWithIntl( - +
Shouldn't be rendered
); @@ -57,7 +57,7 @@ describe('EmptyState component', () => { }, ]; const component = mountWithIntl( - +
Shouldn't appear...
); @@ -66,7 +66,7 @@ describe('EmptyState component', () => { it('renders loading state if no errors or doc count', () => { const component = mountWithIntl( - +
Should appear even while loading...
); @@ -81,7 +81,7 @@ describe('EmptyState component', () => { indexExists: true, }; const component = mountWithIntl( - +
If this is in the snapshot the test should fail
); @@ -91,7 +91,7 @@ describe('EmptyState component', () => { it('notifies when index does not exist', () => { statesIndexStatus.indexExists = false; const component = mountWithIntl( - +
This text should not render
); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx index 023edec023ddf..f8110953f6146 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx @@ -14,48 +14,51 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useContext } from 'react'; +import { UptimeSettingsContext } from '../../../contexts'; interface DataMissingProps { - basePath: string; headingMessage: string; } -export const DataMissing = ({ basePath, headingMessage }: DataMissingProps) => ( - - - - - -

{headingMessage}

- - } - body={ -

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

- } - /> -
-
-
-); +export const DataMissing = ({ headingMessage }: DataMissingProps) => { + const { basePath } = useContext(UptimeSettingsContext); + return ( + + + + + +

{headingMessage}

+ + } + body={ +

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

+ } + /> +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx index 2a39226c5b746..d2d46dff3b9f5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx @@ -18,13 +18,12 @@ interface EmptyStateQueryResult { } interface EmptyStateProps { - basePath: string; children: JSX.Element[] | JSX.Element; } type Props = UptimeGraphQLQueryProps & EmptyStateProps; -export const EmptyStateComponent = ({ basePath, children, data, errors }: Props) => { +export const EmptyStateComponent = ({ children, data, errors }: Props) => { if (errors) { return ; } @@ -33,7 +32,6 @@ export const EmptyStateComponent = ({ basePath, children, data, errors }: Props) if (!indexExists) { return ( ( - - - - {!message - ? i18n.translate('xpack.uptime.emptyStatusBar.defaultMessage', { - defaultMessage: 'No data found for monitor id {monitorId}', - description: - 'This is the default message we display in a status bar when there is no data available for an uptime monitor.', - values: { monitorId }, - }) - : message} - - - -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap index 2022390d0e5d9..0e6ea3662b97e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap @@ -13,6 +13,7 @@ exports[`FilterPopover component does not show item list when loading 1`] = ` /> } closePopover={[Function]} + data-test-subj="filter-popover_test" display="inlineBlock" hasArrow={true} id="test" @@ -49,6 +50,7 @@ exports[`FilterPopover component renders without errors for valid props 1`] = ` /> } closePopover={[Function]} + data-test-subj="filter-popover_test" display="inlineBlock" hasArrow={true} id="test" @@ -83,6 +85,7 @@ exports[`FilterPopover component returns selected items on popover close 1`] = `
{ props = { fieldName: 'foo', id: 'test', - isLoading: false, + loading: false, items: ['first', 'second', 'third', 'fourth'], onFilterFieldChange: jest.fn(), selectedItems: ['first', 'third'], @@ -47,7 +47,7 @@ describe('FilterPopover component', () => { }); it('does not show item list when loading', () => { - props.isLoading = true; + props.loading = true; const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/parse_filter_map.test.ts b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/parse_filter_map.test.ts new file mode 100644 index 0000000000000..8deee25377850 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/parse_filter_map.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseFiltersMap } from '../parse_filter_map'; + +describe('parseFiltersMap', () => { + it('provides values from valid filter string', () => { + expect( + parseFiltersMap( + '[["url.port",["5601","80"]],["observer.geo.name",["us-east-2"]],["monitor.type",["http","tcp"]]]' + ) + ).toMatchSnapshot(); + }); + + it('returns an empty object for invalid filter', () => { + expect(() => parseFiltersMap('some invalid string')).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_group.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_group.tsx index f27514bf76a11..351302fb38356 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_group.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_group.tsx @@ -5,35 +5,49 @@ */ import { EuiFilterGroup } from '@elastic/eui'; -import React from 'react'; -import { get } from 'lodash'; +import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { FilterBar as FilterBarType } from '../../../../common/graphql/types'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; -import { filterBarQuery } from '../../../queries'; +import { connect } from 'react-redux'; import { FilterPopoverProps, FilterPopover } from './filter_popover'; import { FilterStatusButton } from './filter_status_button'; +import { OverviewFilters } from '../../../../common/runtime_types'; +import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; +import { AppState } from '../../../state'; +import { useUrlParams } from '../../../hooks'; +import { parseFiltersMap } from './parse_filter_map'; -interface FilterBarQueryResult { - filters?: FilterBarType; +interface OwnProps { + currentFilter: any; + onFilterUpdate: any; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; } -interface FilterBarDropdownsProps { - currentFilter: string; - onFilterUpdate: (kuery: string) => void; +interface StoreProps { + esKuery: string; + lastRefresh: number; + loading: boolean; + overviewFilters: OverviewFilters; } -type Props = UptimeGraphQLQueryProps & FilterBarDropdownsProps; +interface DispatchProps { + loadFilterGroup: typeof fetchOverviewFilters; +} + +type Props = OwnProps & StoreProps & DispatchProps; + +type PresentationalComponentProps = Pick & + Pick; -export const FilterGroupComponent = ({ - loading: isLoading, +export const PresentationalComponent: React.FC = ({ currentFilter, - data, + overviewFilters, + loading, onFilterUpdate, -}: Props) => { - const locations = get(data, 'filterBar.locations', []); - const ports = get(data, 'filterBar.ports', []); - const schemes = get(data, 'filterBar.schemes', []); +}) => { + const { locations, ports, schemes, tags } = overviewFilters; let filterKueries: Map; try { @@ -67,36 +81,50 @@ export const FilterGroupComponent = ({ const filterPopoverProps: FilterPopoverProps[] = [ { + loading, + onFilterFieldChange, fieldName: 'observer.geo.name', id: 'location', - isLoading, items: locations, - onFilterFieldChange, selectedItems: getSelectedItems('observer.geo.name'), title: i18n.translate('xpack.uptime.filterBar.options.location.name', { defaultMessage: 'Location', }), }, { + loading, + onFilterFieldChange, fieldName: 'url.port', id: 'port', - isLoading, - items: ports, - onFilterFieldChange, + disabled: ports.length === 0, + items: ports.map((p: number) => p.toString()), selectedItems: getSelectedItems('url.port'), title: i18n.translate('xpack.uptime.filterBar.options.portLabel', { defaultMessage: 'Port' }), }, { + loading, + onFilterFieldChange, fieldName: 'monitor.type', id: 'scheme', - isLoading, + disabled: schemes.length === 0, items: schemes, - onFilterFieldChange, selectedItems: getSelectedItems('monitor.type'), title: i18n.translate('xpack.uptime.filterBar.options.schemeLabel', { defaultMessage: 'Scheme', }), }, + { + loading, + onFilterFieldChange, + fieldName: 'tags', + id: 'tags', + disabled: tags.length === 0, + items: tags, + selectedItems: getSelectedItems('tags'), + title: i18n.translate('xpack.uptime.filterBar.options.tagsLabel', { + defaultMessage: 'Tags', + }), + }, ]; return ( @@ -124,7 +152,59 @@ export const FilterGroupComponent = ({ ); }; -export const FilterGroup = withUptimeGraphQL( - FilterGroupComponent, - filterBarQuery -); +export const Container: React.FC = ({ + currentFilter, + esKuery, + filters, + loading, + loadFilterGroup, + dateRangeStart, + dateRangeEnd, + overviewFilters, + statusFilter, + onFilterUpdate, +}: Props) => { + const [getUrlParams] = useUrlParams(); + const { filters: urlFilters } = getUrlParams(); + useEffect(() => { + const filterSelections = parseFiltersMap(urlFilters); + loadFilterGroup({ + dateRangeStart, + dateRangeEnd, + locations: filterSelections.locations ?? [], + ports: filterSelections.ports ?? [], + schemes: filterSelections.schemes ?? [], + search: esKuery, + statusFilter, + tags: filterSelections.tags ?? [], + }); + }, [dateRangeStart, dateRangeEnd, esKuery, filters, statusFilter, urlFilters, loadFilterGroup]); + return ( + + ); +}; + +const mapStateToProps = ({ + overviewFilters: { loading, filters }, + ui: { esKuery, lastRefresh }, +}: AppState): StoreProps => ({ + esKuery, + overviewFilters: filters, + lastRefresh, + loading, +}); + +const mapDispatchToProps = (dispatch: any): DispatchProps => ({ + loadFilterGroup: (payload: GetOverviewFiltersPayload) => dispatch(fetchOverviewFilters(payload)), +}); + +export const FilterGroup = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx index 6e73090782b04..f96fef609fe76 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx @@ -14,7 +14,8 @@ import { LocationLink } from '../monitor_list'; export interface FilterPopoverProps { fieldName: string; id: string; - isLoading: boolean; + loading: boolean; + disabled?: boolean; items: string[]; onFilterFieldChange: (fieldName: string, values: string[]) => void; selectedItems: string[]; @@ -27,7 +28,8 @@ const isItemSelected = (selectedItems: string[], item: string): 'on' | undefined export const FilterPopover = ({ fieldName, id, - isLoading, + disabled, + loading, items, onFilterFieldChange, selectedItems, @@ -48,10 +50,10 @@ export const FilterPopover = ({ }, [searchQuery, items]); return ( - // @ts-ignore zIndex prop is not described in the typing yet 0} numFilters={items.length} numActiveFilters={tempSelectedItems.length} @@ -66,6 +68,7 @@ export const FilterPopover = ({ setIsOpen(false); onFilterFieldChange(fieldName, tempSelectedItems); }} + data-test-subj={`filter-popover_${id}`} id={id} isOpen={isOpen} ownFocus={true} @@ -77,7 +80,7 @@ export const FilterPopover = ({ disabled={items.length === 0} onSearch={query => setSearchQuery(query)} placeholder={ - isLoading + loading ? i18n.translate('xpack.uptime.filterPopout.loadingMessage', { defaultMessage: 'Loading...', }) @@ -90,10 +93,11 @@ export const FilterPopover = ({ } /> - {!isLoading && + {!loading && itemsToDisplay.map(item => ( toggleSelectedItems(item, tempSelectedItems, setTempSelectedItems)} > diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_status_button.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_status_button.tsx index 95f4c30337d62..abbe72530fd80 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_status_button.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_status_button.tsx @@ -11,6 +11,7 @@ import { useUrlParams } from '../../../hooks'; export interface FilterStatusButtonProps { content: string; dataTestSubj: string; + isDisabled?: boolean; value: string; withNext: boolean; } @@ -18,6 +19,7 @@ export interface FilterStatusButtonProps { export const FilterStatusButton = ({ content, dataTestSubj, + isDisabled, value, withNext, }: FilterStatusButtonProps) => { @@ -27,6 +29,7 @@ export const FilterStatusButton = ({ { const nextFilter = { statusFilter: urlValue === value ? '' : value, pagination: '' }; setUrlParams(nextFilter); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/parse_filter_map.ts b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/parse_filter_map.ts new file mode 100644 index 0000000000000..08766521799ea --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/parse_filter_map.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface FilterField { + name: string; + fieldName: string; +} + +/** + * These are the only filter fields we are looking to catch at the moment. + * If your code needs to support custom fields, introduce a second parameter to + * `parseFiltersMap` to take a list of FilterField objects. + */ +const filterWhitelist: FilterField[] = [ + { name: 'ports', fieldName: 'url.port' }, + { name: 'locations', fieldName: 'observer.geo.name' }, + { name: 'tags', fieldName: 'tags' }, + { name: 'schemes', fieldName: 'monitor.type' }, +]; + +export const parseFiltersMap = (filterMapString: string) => { + if (!filterMapString) { + return {}; + } + const filterSlices: { [key: string]: any } = {}; + try { + const map = new Map(JSON.parse(filterMapString)); + filterWhitelist.forEach(({ name, fieldName }) => { + filterSlices[name] = map.get(fieldName) ?? []; + }); + return filterSlices; + } catch { + throw new Error('Unable to parse invalid filter string'); + } +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/uptime_filter_button.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/uptime_filter_button.tsx index fc0c6342bd6e9..0e05c17d57353 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/uptime_filter_button.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/uptime_filter_button.tsx @@ -8,6 +8,7 @@ import { EuiFilterButton } from '@elastic/eui'; import React from 'react'; interface UptimeFilterButtonProps { + isDisabled?: boolean; isSelected: boolean; numFilters: number; numActiveFilters: number; @@ -16,6 +17,7 @@ interface UptimeFilterButtonProps { } export const UptimeFilterButton = ({ + isDisabled, isSelected, numFilters, numActiveFilters, @@ -25,6 +27,7 @@ export const UptimeFilterButton = ({ (undefined); const [isLoadingIndexPattern, setIsLoadingIndexPattern] = useState(true); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); let currentRequestCheck: string; + useIndexPattern((result: any) => setIndexPattern(toStaticIndexPattern(result))); + useEffect(() => { - getIndexPattern(basePath, (result: any) => setIndexPattern(toStaticIndexPattern(result))); - setIsLoadingIndexPattern(false); - }, [basePath]); + if (indexPattern !== undefined) { + setIsLoadingIndexPattern(false); + } + }, [indexPattern]); const [getUrlParams, updateUrlParams] = useUrlParams(); const { search: kuery } = getUrlParams(); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts index 263c3fc787da9..7d53d784ff338 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts @@ -7,6 +7,7 @@ import { getLayerList } from '../map_config'; import { mockLayerList } from './__mocks__/mock'; import { LocationPoint } from '../embedded_map'; +import { UptimeAppColors } from '../../../../../uptime_app'; jest.mock('uuid', () => { return { @@ -17,6 +18,7 @@ jest.mock('uuid', () => { describe('map_config', () => { let upPoints: LocationPoint[]; let downPoints: LocationPoint[]; + let colors: Pick; beforeEach(() => { upPoints = [ @@ -29,11 +31,15 @@ describe('map_config', () => { { lat: '55.487239', lon: '13.399262' }, { lat: '54.487239', lon: '14.399262' }, ]; + colors = { + danger: '#BC261E', + gray: '#000', + }; }); describe('#getLayerList', () => { test('it returns the low poly layer', () => { - const layerList = getLayerList(upPoints, downPoints, { danger: '#BC261E', gray: '#000' }); + const layerList = getLayerList(upPoints, downPoints, colors); expect(layerList).toStrictEqual(mockLayerList); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx index fe8a1a0bad7ec..9b20651fadb86 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState, useContext } from 'react'; +import React, { useEffect, useState, useContext, useRef } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; @@ -48,19 +48,31 @@ const EmbeddedPanel = styled.div` export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => { const { colors } = useContext(UptimeSettingsContext); const [embeddable, setEmbeddable] = useState(); - const embeddableRoot: React.RefObject = React.createRef(); + const embeddableRoot: React.RefObject = useRef(null); const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); const input = { id: uuid.v4(), filters: [], hidePanelTitles: true, - query: { query: '', language: 'kuery' }, - refreshConfig: { value: 0, pause: false }, + query: { + query: '', + language: 'kuery', + }, + refreshConfig: { + value: 0, + pause: false, + }, viewMode: 'view', isLayerTOCOpen: false, hideFilterActions: true, - mapCenter: { lon: 11, lat: 20, zoom: 0 }, + // Zoom Lat/Lon values are set to make sure map is in center in the panel + // It wil also omit Greenland/Antarctica etc + mapCenter: { + lon: 11, + lat: 20, + zoom: 0, + }, disableInteractive: true, disableTooltipControl: true, hideToolbarOverlay: true, @@ -80,16 +92,19 @@ export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => { setEmbeddable(embeddableObject); } setupEmbeddable(); + // we want this effect to execute exactly once after the component mounts // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // update map layers based on points useEffect(() => { if (embeddable) { embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); } }, [upPoints, downPoints, embeddable, colors]); + // We can only render after embeddable has already initialized useEffect(() => { if (embeddableRoot.current && embeddable) { embeddable.render(embeddableRoot.current); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts index b423b8baf41bf..d4601baefdf30 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts @@ -6,6 +6,7 @@ import lowPolyLayerFeatures from './low_poly_layer.json'; import { LocationPoint } from './embedded_map'; +import { UptimeAppColors } from '../../../../uptime_app'; /** * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, @@ -15,7 +16,7 @@ import { LocationPoint } from './embedded_map'; export const getLayerList = ( upPoints: LocationPoint[], downPoints: LocationPoint[], - { gray, danger }: { gray: string; danger: string } + { gray, danger }: Pick ) => { return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx index f70d145ec05c3..9a9bf3fe71dc1 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx @@ -11,10 +11,12 @@ import { LocationStatusTags } from './location_status_tags'; import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; import { MonitorLocations } from '../../../../common/runtime_types'; +// These height/width values are used to make sure map is in center of panel +// And to make sure, it doesn't take too much space const MapPanel = styled.div` height: 240px; width: 520px; - margin-right: 10px; + margin-right: 20px; `; interface LocationMapProps { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json index 64adf3642fb22..a45e974685b9c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json @@ -271,7 +271,6 @@ "monitor": { "id": null, "name": "elastic", - "status": "mixed", "type": null, "__typename": "MonitorState" }, @@ -377,7 +376,6 @@ "monitor": { "id": null, "name": null, - "status": "mixed", "type": null, "__typename": "MonitorState" }, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx index aca43f550aa14..5c606f2356dfc 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -8,11 +8,12 @@ import { MonitorSummary, Check } from '../../../../../../common/graphql/types'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { MonitorListDrawerComponent } from '../monitor_list_drawer'; +import { MonitorDetails } from '../../../../../../common/runtime_types'; describe('MonitorListDrawer component', () => { let summary: MonitorSummary; let loadMonitorDetails: any; - let monitorDetails: any; + let monitorDetails: MonitorDetails; beforeEach(() => { summary = { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx index d793e60dcd089..35b649fa35795 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -6,7 +6,6 @@ import React, { useEffect } from 'react'; import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; -import { get } from 'lodash'; import styled from 'styled-components'; import { connect } from 'react-redux'; import { MonitorSummary } from '../../../../../common/graphql/types'; @@ -16,6 +15,8 @@ import { MostRecentError } from './most_recent_error'; import { getMonitorDetails } from '../../../../state/selectors'; import { MonitorStatusList } from './monitor_status_list'; import { MonitorDetails } from '../../../../../common/runtime_types'; +import { useUrlParams } from '../../../../hooks'; +import { MonitorDetailsActionPayload } from '../../../../state/actions/types'; import { MonitorListActionsPopover } from '../monitor_list_actions_popover'; const ContainerDiv = styled.div` @@ -50,19 +51,20 @@ export function MonitorListDrawerComponent({ monitorDetails, }: MonitorListDrawerProps) { const monitorId = summary?.monitor_id; - useEffect(() => { - if (monitorId) { - loadMonitorDetails(monitorId); - } - }, [loadMonitorDetails, monitorId]); + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); - if (!summary || !summary.state.checks) { - return null; - } + useEffect(() => { + loadMonitorDetails({ + dateStart, + dateEnd, + monitorId, + }); + }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); - const monitorUrl: string | undefined = get(summary.state.url, 'full', undefined); + const monitorUrl = summary?.state?.url?.full || ''; - return ( + return summary && summary.state.checks ? ( @@ -87,7 +89,7 @@ export function MonitorListDrawerComponent({ /> )} - ); + ) : null; } const mapStateToProps = (state: AppState, { summary }: any) => ({ @@ -95,7 +97,8 @@ const mapStateToProps = (state: AppState, { summary }: any) => ({ }); const mapDispatchToProps = (dispatch: any) => ({ - loadMonitorDetails: (monitorId: string) => dispatch(fetchMonitorDetails(monitorId)), + loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => + dispatch(fetchMonitorDetails(actionPayload)), }); export const MonitorListDrawer = connect( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx index 463048512e1e0..0a3a0962a4d09 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx @@ -20,8 +20,6 @@ const getHealthColor = (status: string): string => { return 'success'; case 'down': return 'danger'; - case 'mixed': - return 'warning'; default: return ''; } @@ -37,10 +35,6 @@ const getHealthMessage = (status: string): string | null => { return i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { defaultMessage: 'Down', }); - case 'mixed': - return i18n.translate('xpack.uptime.monitorList.statusColumn.mixedLabel', { - defaultMessage: 'Mixed', - }); default: return null; } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_title.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_title.tsx deleted file mode 100644 index 9e30a3adbd776..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_title.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiTextColor, EuiTitle } from '@elastic/eui'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; -import { MonitorPageTitle as TitleType } from '../../../common/graphql/types'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; -import { monitorPageTitleQuery } from '../../queries'; - -interface MonitorPageTitleQueryResult { - monitorPageTitle?: TitleType; -} - -interface MonitorPageTitleProps { - monitorId: string; -} - -type Props = MonitorPageTitleProps & UptimeGraphQLQueryProps; - -export const MonitorPageTitleComponent = ({ data }: Props) => - data && data.monitorPageTitle ? ( - - -

{data.monitorPageTitle.id}

-
-
- ) : ( - - ); - -export const MonitorPageTitle = withUptimeGraphQL< - MonitorPageTitleQueryResult, - MonitorPageTitleProps ->(MonitorPageTitleComponent, monitorPageTitleQuery); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/index.ts deleted file mode 100644 index 7087407407c55..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { MonitorSSLCertificate } from './monitor_ssl_certificate'; -export { MonitorStatusBar, MonitorStatusBarComponent } from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx deleted file mode 100644 index f36f0dff6745f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx +++ /dev/null @@ -1,80 +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, EuiHealth, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import moment from 'moment'; -import React from 'react'; -import { Ping } from '../../../../common/graphql/types'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; -import { monitorStatusBarQuery } from '../../../queries'; -import { EmptyStatusBar } from '../empty_status_bar'; -import { convertMicrosecondsToMilliseconds } from '../../../lib/helper'; -import { MonitorSSLCertificate } from './monitor_ssl_certificate'; -import * as labels from './translations'; - -interface MonitorStatusBarQueryResult { - monitorStatus?: Ping[]; -} - -interface MonitorStatusBarProps { - monitorId: string; -} - -type Props = MonitorStatusBarProps & UptimeGraphQLQueryProps; - -export const MonitorStatusBarComponent = ({ data, monitorId }: Props) => { - if (data?.monitorStatus?.length) { - const { monitor, timestamp, tls } = data.monitorStatus[0]; - const duration: number | undefined = get(monitor, 'duration.us', undefined); - const status = get<'up' | 'down'>(monitor, 'status', 'down'); - const full = get(data.monitorStatus[0], 'url.full'); - - return ( - <> - - - - {status === 'up' ? labels.upLabel : labels.downLabel} - - - - - - {full} - - - - {!!duration && ( - - - - )} - - {moment(new Date(timestamp).valueOf()).fromNow()} - - - - - ); - } - return ; -}; - -export const MonitorStatusBar = withUptimeGraphQL< - MonitorStatusBarQueryResult, - MonitorStatusBarProps ->(MonitorStatusBarComponent, monitorStatusBarQuery); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap new file mode 100644 index 0000000000000..c6b18501ffa0f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StatusByLocation component renders all locations are down 1`] = ` +
+

+ Down in 2 Locations +

+
+`; + +exports[`StatusByLocation component renders when down in some locations 1`] = ` +
+

+ Down in 1/2 Locations +

+
+`; + +exports[`StatusByLocation component renders when only one location and it is down 1`] = ` +
+

+ Down in 1 Location +

+
+`; + +exports[`StatusByLocation component renders when only one location and it is up 1`] = ` +
+

+ Up in 1 Location +

+
+`; + +exports[`StatusByLocation component renders when up in all locations 1`] = ` +
+

+ Up in 2 Locations +

+
+`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/status_by_location.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/status_by_location.test.tsx new file mode 100644 index 0000000000000..4e515a52b8de6 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/status_by_location.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; +import { MonitorLocation } from '../../../../../common/runtime_types'; +import { StatusByLocations } from '../'; + +describe('StatusByLocation component', () => { + let monitorLocations: MonitorLocation[]; + + it('renders when up in all locations', () => { + monitorLocations = [ + { + summary: { up: 4, down: 0 }, + geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + { + summary: { up: 4, down: 0 }, + geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + ]; + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders when only one location and it is up', () => { + monitorLocations = [ + { + summary: { up: 4, down: 0 }, + geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + ]; + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders when only one location and it is down', () => { + monitorLocations = [ + { + summary: { up: 0, down: 4 }, + geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + ]; + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders all locations are down', () => { + monitorLocations = [ + { + summary: { up: 0, down: 4 }, + geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + { + summary: { up: 0, down: 4 }, + geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + ]; + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders when down in some locations', () => { + monitorLocations = [ + { + summary: { up: 0, down: 4 }, + geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + { + summary: { up: 4, down: 0 }, + geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + }, + ]; + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts index 234586e0b51f1..7b4e1ea353c11 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts @@ -5,12 +5,12 @@ */ import { connect } from 'react-redux'; import { AppState } from '../../../state'; -import { getMonitorLocations } from '../../../state/selectors'; +import { selectMonitorLocations } from '../../../state/selectors'; import { fetchMonitorLocations } from '../../../state/actions/monitor'; import { MonitorStatusDetailsComponent } from './monitor_status_details'; const mapStateToProps = (state: AppState, { monitorId }: any) => ({ - monitorLocations: getMonitorLocations(state, monitorId), + monitorLocations: selectMonitorLocations(state, monitorId), }); const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ @@ -32,3 +32,5 @@ export const MonitorStatusDetails = connect( )(MonitorStatusDetailsComponent); export * from './monitor_status_details'; +export { MonitorStatusBar } from './monitor_status_bar'; +export { StatusByLocations } from './monitor_status_bar/status_by_location'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts new file mode 100644 index 0000000000000..94bd7fa7f026b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { + StateProps, + DispatchProps, + MonitorStatusBarComponent, + MonitorStatusBarProps, +} from './monitor_status_bar'; +import { selectMonitorStatus, selectMonitorLocations } from '../../../../state/selectors'; +import { AppState } from '../../../../state'; +import { getMonitorStatus, getSelectedMonitor } from '../../../../state/actions'; + +const mapStateToProps = (state: AppState, ownProps: MonitorStatusBarProps) => ({ + monitorStatus: selectMonitorStatus(state), + monitorLocations: selectMonitorLocations(state, ownProps.monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch, ownProps: MonitorStatusBarProps) => ({ + loadMonitorStatus: () => { + const { dateStart, dateEnd, monitorId } = ownProps; + dispatch( + getMonitorStatus({ + monitorId, + dateStart, + dateEnd, + }) + ); + dispatch( + getSelectedMonitor({ + monitorId, + }) + ); + }, +}); + +// @ts-ignore TODO: Investigate typescript issues here +export const MonitorStatusBar = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(MonitorStatusBarComponent); + +export { MonitorSSLCertificate } from './monitor_ssl_certificate'; +export * from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_ssl_certificate.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_ssl_certificate.tsx similarity index 53% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_ssl_certificate.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_ssl_certificate.tsx index c50f7f1b00f0d..5e916c40e712d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_ssl_certificate.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_ssl_certificate.tsx @@ -10,9 +10,8 @@ import moment from 'moment'; import { EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { PingTls } from '../../../../common/graphql/types'; +import { PingTls } from '../../../../../common/graphql/types'; interface Props { /** @@ -21,10 +20,6 @@ interface Props { tls: PingTls | null | undefined; } -const TextContainer = styled.div` - margin-left: 20px; -`; - export const MonitorSSLCertificate = ({ tls }: Props) => { const certificateValidity: string | undefined = get( tls, @@ -37,27 +32,22 @@ export const MonitorSSLCertificate = ({ tls }: Props) => { return validExpiryDate && certificateValidity ? ( <> - - - - - + + + ) : null; }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx new file mode 100644 index 0000000000000..57ca909ffde55 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, + EuiTitle, + EuiTextColor, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { MonitorSSLCertificate } from './monitor_ssl_certificate'; +import * as labels from './translations'; +import { StatusByLocations } from './status_by_location'; +import { Ping } from '../../../../../common/graphql/types'; +import { MonitorLocations } from '../../../../../common/runtime_types'; + +export interface StateProps { + monitorStatus: Ping; + monitorLocations: MonitorLocations; +} + +export interface DispatchProps { + loadMonitorStatus: () => void; +} + +export interface MonitorStatusBarProps { + monitorId: string; + dateStart: string; + dateEnd: string; +} + +type Props = MonitorStatusBarProps & StateProps & DispatchProps; + +export const MonitorStatusBarComponent: React.FC = ({ + dateStart, + dateEnd, + monitorId, + loadMonitorStatus, + monitorStatus, + monitorLocations, +}) => { + useEffect(() => { + loadMonitorStatus(); + }, [dateStart, dateEnd, loadMonitorStatus]); + + const full = monitorStatus?.url?.full ?? ''; + + return ( + + + + + + + + {full} + + + + + + +

{monitorId}

+
+
+
+ + + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/status_by_location.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/status_by_location.tsx new file mode 100644 index 0000000000000..461ffc10124fd --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/status_by_location.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MonitorLocation } from '../../../../../common/runtime_types'; + +interface StatusByLocationsProps { + locations: MonitorLocation[]; +} + +export const StatusByLocations = ({ locations }: StatusByLocationsProps) => { + const upLocations: string[] = []; + const downLocations: string[] = []; + + if (locations) + locations.forEach((item: any) => { + if (item.summary.down === 0) { + upLocations.push(item.geo.name); + } else { + downLocations.push(item.geo.name); + } + }); + + let statusMessage = ''; + let status = ''; + if (downLocations.length === 0) { + // for Messaging like 'Up in 1 Location' or 'Up in 2 Locations' + statusMessage = `${locations.length}`; + status = 'Up'; + } else if (downLocations.length > 0) { + // for Messaging like 'Down in 1/2 Locations' + status = 'Down'; + statusMessage = `${downLocations.length}/${locations.length}`; + if (downLocations.length === locations.length) { + // for Messaging like 'Down in 2 Locations' + statusMessage = `${locations.length}`; + } + } + + return ( + +

+ {locations.length <= 1 ? ( + + ) : ( + + )} +

+
+ ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/translations.ts rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx index ed67c6364e958..bb87497d335ef 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx @@ -7,7 +7,7 @@ import React, { useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { LocationMap } from '../location_map'; -import { MonitorStatusBar } from '../monitor_status_bar'; +import { MonitorStatusBar } from './monitor_status_bar'; interface MonitorStatusBarProps { monitorId: string; @@ -34,7 +34,12 @@ export const MonitorStatusDetailsComponent = ({ - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 4c1b482b198af..90d716001cff9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -43,7 +43,7 @@ interface StoreProps { /** * Contains functions that will dispatch actions used - * for this component's lifecyclel + * for this component's life cycle */ interface DispatchProps { loadSnapshotCount: typeof fetchSnapshotCount; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx index ebd0cd1e4ae85..c282ac9b9e155 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx @@ -5,9 +5,10 @@ */ import { EuiSuperDatePicker } from '@elastic/eui'; -import React from 'react'; +import React, { useContext } from 'react'; import { useUrlParams } from '../../hooks'; import { CLIENT_DEFAULTS } from '../../../common/constants'; +import { UptimeSettingsContext } from '../../contexts'; // TODO: when EUI exports types for this, this should be replaced interface SuperDateRangePickerRangeChangedEvent { @@ -26,16 +27,14 @@ export interface CommonlyUsedRange { display: string; } -interface Props { +interface UptimeDatePickerProps { refreshApp: () => void; - commonlyUsedRanges?: CommonlyUsedRange[]; } -type UptimeDatePickerProps = Props; - -export const UptimeDatePicker = ({ refreshApp, commonlyUsedRanges }: UptimeDatePickerProps) => { +export const UptimeDatePicker = ({ refreshApp }: UptimeDatePickerProps) => { const [getUrlParams, updateUrl] = useUrlParams(); const { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd } = getUrlParams(); + const { commonlyUsedRanges } = useContext(UptimeSettingsContext); const euiCommonlyUsedRanges = commonlyUsedRanges ? commonlyUsedRanges.map( diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.ts b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.ts index 0fd9be952ed40..c656391678aa2 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.ts +++ b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.ts @@ -9,6 +9,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { createContext } from 'react'; import { UptimeAppColors } from '../uptime_app'; import { CONTEXT_DEFAULTS } from '../../common/constants'; +import { CommonlyUsedRange } from '../components/functional/uptime_date_picker'; export interface UMSettingsContextValues { absoluteStartDate: number; @@ -23,7 +24,7 @@ export interface UMSettingsContextValues { isInfraAvailable: boolean; isLogsAvailable: boolean; refreshApp: () => void; - setHeadingText: (text: string) => void; + commonlyUsedRanges?: CommonlyUsedRange[]; } const { @@ -64,9 +65,6 @@ const defaultContext: UMSettingsContextValues = { refreshApp: () => { throw new Error('App refresh was not initialized, set it when you invoke the context'); }, - setHeadingText: () => { - throw new Error('setHeadingText was not initialized on UMSettingsContext.'); - }, }; export const UptimeSettingsContext = createContext(defaultContext); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/index.ts b/x-pack/legacy/plugins/uptime/public/hooks/index.ts index 22de59833b08d..aa7bb0a220357 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/index.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/index.ts @@ -5,3 +5,5 @@ */ export { useUrlParams } from './use_url_params'; +export { useIndexPattern } from './use_index_pattern'; +export * from './use_telemetry'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts new file mode 100644 index 0000000000000..f2b586b27dba6 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, Dispatch } from 'react'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +export const useIndexPattern = (setIndexPattern: Dispatch) => { + const core = useKibana(); + useEffect(() => { + const fetch = core.services.http?.fetch; + async function getIndexPattern() { + if (!fetch) throw new Error('Http core services are not defined'); + setIndexPattern(await fetch('/api/uptime/index_pattern', { method: 'GET' })); + } + getIndexPattern(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [core.services.http]); +}; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts new file mode 100644 index 0000000000000..15f276174e2cf --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; +import { HttpHandler } from 'kibana/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +export enum UptimePage { + Overview = '/api/uptime/logOverview', + Monitor = '/api/uptime/logMonitor', + NotFound = '__not-found__', +} + +const getApiPath = (page?: UptimePage) => { + if (!page) throw new Error('Telemetry logging for this page not yet implemented'); + if (page === '__not-found__') + throw new Error('Telemetry logging for 404 page not yet implemented'); + return page.valueOf(); +}; + +const logPageLoad = async (fetch: HttpHandler, page?: UptimePage) => { + try { + await fetch(getApiPath(page), { + method: 'POST', + }); + } catch (e) { + throw e; + } +}; + +export const useUptimeTelemetry = (page?: UptimePage) => { + const kibana = useKibana(); + const fetch = kibana.services.http?.fetch; + useEffect(() => { + if (!fetch) throw new Error('Core http services are not defined'); + logPageLoad(fetch, page); + }, [fetch, page]); +}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index b7ff3b2aa6264..28179c229013b 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChromeBreadcrumb, CoreStart } from 'src/core/public'; +import { ChromeBreadcrumb, LegacyCoreStart } from 'src/core/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { get } from 'lodash'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { i18n as i18nFormatter } from '@kbn/i18n'; +import { PluginsStart } from 'ui/new_platform/new_platform'; import { CreateGraphQLClient } from './framework_adapter_types'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; @@ -19,13 +19,12 @@ import { DEFAULT_DARK_MODE, DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../../../common/constants'; -import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry'; import { UMFrameworkAdapter, BootstrapUptimeApp } from '../../lib'; import { createApolloClient } from './apollo_client_adapter'; export const getKibanaFrameworkAdapter = ( - core: CoreStart, - autocomplete: Pick + core: LegacyCoreStart, + plugins: PluginsStart ): UMFrameworkAdapter => { const { application: { capabilities }, @@ -44,10 +43,10 @@ export const getKibanaFrameworkAdapter = ( ); const canSave = get(capabilities, 'uptime.save', false); const props: UptimeAppProps = { - autocomplete, basePath: basePath.get(), canSave, client: createApolloClient(`${basePath.get()}/api/uptime/graphql`, 'true'), + core, darkMode: core.uiSettings.get(DEFAULT_DARK_MODE), commonlyUsedRanges: core.uiSettings.get(DEFAULT_TIMEPICKER_QUICK_RANGES), i18n, @@ -55,8 +54,7 @@ export const getKibanaFrameworkAdapter = ( isInfraAvailable: infrastructure, isLogsAvailable: logs, kibanaBreadcrumbs: breadcrumbs, - logMonitorPageLoad: getTelemetryMonitorPageLogger('true', basePath.get()), - logOverviewPageLoad: getTelemetryOverviewPageLogger('true', basePath.get()), + plugins, renderGlobalHelpControls: () => setHelpExtension({ appName: i18nFormatter.translate('xpack.uptime.header.appName', { diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts deleted file mode 100644 index 6654def2f944b..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios, { AxiosRequestConfig } from 'axios'; -import { getIndexPattern } from '../get_index_pattern'; - -describe('getIndexPattern', () => { - let axiosSpy: jest.SpyInstance, [string, (AxiosRequestConfig | undefined)?]>; - beforeEach(() => { - axiosSpy = jest.spyOn(axios, 'get'); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('returns expected data', async () => { - expect.assertions(3); - axiosSpy.mockReturnValue(new Promise(r => r({ data: { foo: 'bar' } }))); - expect(await getIndexPattern()).toEqual({ foo: 'bar' }); - expect(axiosSpy.mock.calls).toHaveLength(1); - expect(axiosSpy.mock.calls[0]).toEqual(['/api/uptime/index_pattern']); - }); - - it('handles the supplied basePath', async () => { - expect.assertions(2); - await getIndexPattern('foo'); - expect(axiosSpy.mock.calls).toHaveLength(1); - expect(axiosSpy.mock.calls[0]).toEqual(['foo/api/uptime/index_pattern']); - }); - - it('supplies the returned data to the given setter function', async () => { - const mockSetter = jest.fn(); - axiosSpy.mockReturnValue(new Promise(r => r({ data: { foo: 'bar' } }))); - await getIndexPattern(undefined, mockSetter); - expect(mockSetter).toHaveBeenCalled(); - expect(mockSetter).toHaveBeenCalledWith({ foo: 'bar' }); - }); - - it('returns undefined when there is an error fetching', async () => { - expect.assertions(1); - axiosSpy.mockReturnValue( - new Promise((resolve, reject) => reject('Request timeout, server could not be reached')) - ); - expect(await getIndexPattern()).toBeUndefined(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts deleted file mode 100644 index fd4161b35f7dd..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios from 'axios'; -import { getApiPath } from '../../helper'; - -/** - * Fetches and returns the uptime index pattern, optionally provides it to - * a given setter function. - * @param basePath - the base path, if any - * @param setter - a callback for use with non-async functions like `useEffect` - */ -export const getIndexPattern = async (basePath?: string, setter?: (data: unknown) => void) => { - try { - const { data } = await axios.get(getApiPath('/api/uptime/index_pattern', basePath)); - if (setter) { - setter(data); - } - return data; - } catch { - return undefined; - } -}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts deleted file mode 100644 index 20328497d69a8..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios from 'axios'; -import { getApiPath } from '../../helper'; - -/** - * Generates a function to log a page load of the monitor page for Kibana telemetry. - * @returns a function that can log page loads - */ -export const getTelemetryMonitorPageLogger = (xsrf: string, basePath?: string) => async () => { - await axios.post(getApiPath('/api/uptime/logMonitor', basePath), undefined, { - headers: { 'kbn-xsrf': xsrf }, - }); -}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts deleted file mode 100644 index fd9fd773a18b9..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios from 'axios'; -import { getApiPath } from '../../helper'; - -/** - * Generates a function to log a page load of the overview page for Kibana telemtry. - * @returns a function that can log page loads - */ -export const getTelemetryOverviewPageLogger = (xsrf: string, basePath?: string) => async () => { - await axios.post(getApiPath('/api/uptime/logOverview', basePath), undefined, { - headers: { 'kbn-xsrf': xsrf }, - }); -}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap new file mode 100644 index 0000000000000..39c28a87f5e71 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parameterizeValues parameterizes provided values for multiple fields 1`] = `"foo=bar&foo=baz&bar=foo&bar=baz"`; + +exports[`parameterizeValues parameterizes the provided values for a given field name 1`] = `"foo=bar&foo=baz"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts new file mode 100644 index 0000000000000..e550a1a6397e3 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parameterizeValues } from '../parameterize_values'; + +describe('parameterizeValues', () => { + let params: URLSearchParams; + + beforeEach(() => { + params = new URLSearchParams(); + }); + + it('parameterizes the provided values for a given field name', () => { + parameterizeValues(params, { foo: ['bar', 'baz'] }); + expect(params.toString()).toMatchSnapshot(); + }); + + it('parameterizes provided values for multiple fields', () => { + parameterizeValues(params, { foo: ['bar', 'baz'], bar: ['foo', 'baz'] }); + expect(params.toString()).toMatchSnapshot(); + }); + + it('returns an empty string when there are no values provided', () => { + parameterizeValues(params, { foo: [] }); + expect(params.toString()).toBe(''); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts index a4cfb1c51b0ec..ced06ce7a1d7b 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts @@ -9,6 +9,7 @@ export { convertMicrosecondsToMilliseconds } from './convert_measurements'; export * from './observability_integration'; export { getApiPath } from './get_api_path'; export { getChartDateLabel } from './charts'; +export { parameterizeValues } from './parameterize_values'; export { seriesHasDownValues } from './series_has_down_values'; export { stringifyKueries } from './stringify_kueries'; export { toStaticIndexPattern } from './to_static_index_pattern'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts similarity index 54% rename from x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts rename to x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts index 7a6c7f71bc98c..4c9fa6838c2ed 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export const logout = (): null => { - cy.request({ - method: 'GET', - url: `${Cypress.config().baseUrl}/logout`, - }).then(response => { - expect(response.status).to.eq(200); +export const parameterizeValues = ( + params: URLSearchParams, + obj: Record +): void => { + Object.keys(obj).forEach(key => { + obj[key].forEach(val => { + params.append(key, val); + }); }); - return null; }; diff --git a/x-pack/legacy/plugins/uptime/public/pages/index.ts b/x-pack/legacy/plugins/uptime/public/pages/index.ts index bf5d55ebf17ae..a96be42eb0dee 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/index.ts +++ b/x-pack/legacy/plugins/uptime/public/pages/index.ts @@ -7,3 +7,4 @@ export { MonitorPage } from './monitor'; export { OverviewPage } from './overview'; export { NotFoundPage } from './not_found'; +export { PageHeader } from './page_header'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 8c5649f680fcb..1b4ad8d82ead1 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -5,66 +5,31 @@ */ import { EuiSpacer } from '@elastic/eui'; -import { ApolloQueryResult, OperationVariables, QueryOptions } from 'apollo-client'; -import gql from 'graphql-tag'; -import React, { Fragment, useContext, useEffect, useState } from 'react'; -import { getMonitorPageBreadcrumb } from '../breadcrumbs'; -import { MonitorCharts, MonitorPageTitle, PingList } from '../components/functional'; +import React, { Fragment, useContext, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { MonitorCharts, PingList } from '../components/functional'; import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeSettingsContext } from '../contexts'; -import { useUrlParams } from '../hooks'; -import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; +import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../infra/public'; -import { getTitle } from '../lib/helper/get_title'; import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; +import { PageHeader } from './page_header'; interface MonitorPageProps { - logMonitorPageLoad: () => void; - match: { params: { monitorId: string } }; - // this is the query function provided by Apollo's Client API - query: ( - options: QueryOptions - ) => Promise>; setBreadcrumbs: UMUpdateBreadcrumbs; } -export const MonitorPage = ({ - logMonitorPageLoad, - query, - setBreadcrumbs, - match, -}: MonitorPageProps) => { +export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { // decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url - const monitorId = atob(match.params.monitorId); + let { monitorId } = useParams(); + monitorId = atob(monitorId || ''); + const [pingListPageCount, setPingListPageCount] = useState(10); - const { colors, refreshApp, setHeadingText } = useContext(UptimeSettingsContext); + const { colors, refreshApp } = useContext(UptimeSettingsContext); const [getUrlParams, updateUrlParams] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); const { dateRangeStart, dateRangeEnd, selectedPingStatus } = params; - useEffect(() => { - query({ - query: gql` - query MonitorPageTitle($monitorId: String!) { - monitorPageTitle: getMonitorPageTitle(monitorId: $monitorId) { - id - url - name - } - } - `, - variables: { monitorId }, - }).then((result: any) => { - const { name, url, id } = result.data.monitorPageTitle; - const heading: string = name || url || id; - document.title = getTitle(name); - setBreadcrumbs(getMonitorPageBreadcrumb(heading, stringifyUrlParams(params))); - if (setHeadingText) { - setHeadingText(heading); - } - }); - }, [monitorId, params, query, setBreadcrumbs, setHeadingText]); - const [selectedLocation, setSelectedLocation] = useState(undefined); const sharedVariables = { @@ -74,16 +39,14 @@ export const MonitorPage = ({ monitorId, }; - useEffect(() => { - logMonitorPageLoad(); - }, [logMonitorPageLoad]); + useUptimeTelemetry(UptimePage.Monitor); useTrackPageview({ app: 'uptime', path: 'monitor' }); useTrackPageview({ app: 'uptime', path: 'monitor', delay: 15000 }); return ( - + ; - history: any; - location: { - pathname: string; - search: string; - }; - logOverviewPageLoad: () => void; setBreadcrumbs: UMUpdateBreadcrumbs; } @@ -54,13 +47,8 @@ const EuiFlexItemStyled = styled(EuiFlexItem)` } `; -export const OverviewPage = ({ - basePath, - autocomplete, - logOverviewPageLoad, - setBreadcrumbs, -}: Props) => { - const { colors, setHeadingText } = useContext(UptimeSettingsContext); +export const OverviewPage = ({ autocomplete, setBreadcrumbs }: Props) => { + const { colors } = useContext(UptimeSettingsContext); const [getUrlParams, updateUrl] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); const { @@ -72,25 +60,12 @@ export const OverviewPage = ({ filters: urlFilters, } = params; const [indexPattern, setIndexPattern] = useState(undefined); - - useEffect(() => { - getIndexPattern(basePath, setIndexPattern); - setBreadcrumbs(getOverviewPageBreadcrumbs()); - logOverviewPageLoad(); - if (setHeadingText) { - setHeadingText( - i18n.translate('xpack.uptime.overviewPage.headerText', { - defaultMessage: 'Overview', - description: `The text that will be displayed in the app's heading when the Overview page loads.`, - }) - ); - } - }, [basePath, logOverviewPageLoad, setBreadcrumbs, setHeadingText]); + useUptimeTelemetry(UptimePage.Overview); + useIndexPattern(setIndexPattern); useTrackPageview({ app: 'uptime', path: 'overview' }); useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 }); - const filterQueryString = search || ''; let error: any; let kueryString: string = ''; try { @@ -102,6 +77,7 @@ export const OverviewPage = ({ kueryString = ''; } + const filterQueryString = search || ''; let filters: any | undefined; try { if (filterQueryString || urlFilters) { @@ -111,6 +87,15 @@ export const OverviewPage = ({ const ast = esKuery.fromKueryExpression(combinedFilterString); const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, staticIndexPattern); filters = JSON.stringify(elasticsearchQuery); + const searchDSL: string = filterQueryString + ? JSON.stringify( + esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(filterQueryString), + staticIndexPattern + ) + ) + : ''; + store.dispatch(setEsKueryString(searchDSL)); } } } catch (e) { @@ -128,20 +113,21 @@ export const OverviewPage = ({ return ( - + + { if (urlFilters !== filtersKuery) { updateUrl({ filters: filtersKuery, pagination: '' }); } }} - variables={sharedProps} /> {error && } diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx new file mode 100644 index 0000000000000..250dacb8914e7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useState, useContext } from 'react'; +import { connect } from 'react-redux'; +import { useRouteMatch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { UptimeDatePicker } from '../components/functional/uptime_date_picker'; +import { AppState } from '../state'; +import { selectSelectedMonitor } from '../state/selectors'; +import { getMonitorPageBreadcrumb, getOverviewPageBreadcrumbs } from '../breadcrumbs'; +import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; +import { UptimeSettingsContext } from '../contexts'; +import { getTitle } from '../lib/helper/get_title'; +import { UMUpdateBreadcrumbs } from '../lib/lib'; +import { MONITOR_ROUTE } from '../routes'; + +interface PageHeaderProps { + monitorStatus?: any; + setBreadcrumbs: UMUpdateBreadcrumbs; +} + +export const PageHeaderComponent = ({ monitorStatus, setBreadcrumbs }: PageHeaderProps) => { + const monitorPage = useRouteMatch({ + path: MONITOR_ROUTE, + }); + const { refreshApp } = useContext(UptimeSettingsContext); + + const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useParams(); + + const headingText = i18n.translate('xpack.uptime.overviewPage.headerText', { + defaultMessage: 'Overview', + description: `The text that will be displayed in the app's heading when the Overview page loads.`, + }); + + const [headerText, setHeaderText] = useState(headingText); + + useEffect(() => { + if (monitorPage) { + setHeaderText(monitorStatus?.url?.full); + if (monitorStatus?.monitor) { + const { name, id } = monitorStatus.monitor; + document.title = getTitle(name || id); + } + } else { + document.title = getTitle(); + } + }, [monitorStatus, monitorPage, setHeaderText]); + + useEffect(() => { + if (monitorPage) { + if (headerText) { + setBreadcrumbs(getMonitorPageBreadcrumb(headerText, stringifyUrlParams(params))); + } + } else { + setBreadcrumbs(getOverviewPageBreadcrumbs()); + } + }, [headerText, setBreadcrumbs, params, monitorPage]); + + return ( + <> + + + +

{headerText}

+
+
+ + + +
+ + + ); +}; + +const mapStateToProps = (state: AppState) => ({ + monitorStatus: selectSelectedMonitor(state), +}); + +export const PageHeader = connect(mapStateToProps, null)(PageHeaderComponent); diff --git a/x-pack/legacy/plugins/uptime/public/queries/filter_bar_query.ts b/x-pack/legacy/plugins/uptime/public/queries/filter_bar_query.ts deleted file mode 100644 index a9b7e52c0f793..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/filter_bar_query.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const filterBarQueryString = ` -query FilterBar($dateRangeStart: String!, $dateRangeEnd: String!) { - filterBar: getFilterBar(dateRangeStart: $dateRangeStart, dateRangeEnd: $dateRangeEnd) { - ids - locations - ports - schemes - urls - } -} -`; - -export const filterBarQuery = gql` - ${filterBarQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/queries/index.ts b/x-pack/legacy/plugins/uptime/public/queries/index.ts index b86522c03aba8..02c9c7cb23403 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/index.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/index.ts @@ -5,8 +5,5 @@ */ export { docCountQuery, docCountQueryString } from './doc_count_query'; -export { filterBarQuery, filterBarQueryString } from './filter_bar_query'; export { monitorChartsQuery, monitorChartsQueryString } from './monitor_charts_query'; -export { monitorPageTitleQuery } from './monitor_page_title_query'; -export { monitorStatusBarQuery, monitorStatusBarQueryString } from './monitor_status_bar_query'; export { pingsQuery, pingsQueryString } from './pings_query'; diff --git a/x-pack/legacy/plugins/uptime/public/queries/monitor_page_title_query.ts b/x-pack/legacy/plugins/uptime/public/queries/monitor_page_title_query.ts deleted file mode 100644 index 3b59ef80183f7..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/monitor_page_title_query.ts +++ /dev/null @@ -1,20 +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 gql from 'graphql-tag'; - -export const monitorPageTitleQueryString = ` -query MonitorPageTitle($monitorId: String!) { - monitorPageTitle: getMonitorPageTitle(monitorId: $monitorId) { - id - url - name - } -}`; - -export const monitorPageTitleQuery = gql` - ${monitorPageTitleQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/queries/monitor_status_bar_query.ts b/x-pack/legacy/plugins/uptime/public/queries/monitor_status_bar_query.ts deleted file mode 100644 index 1f0eed6bb366a..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/monitor_status_bar_query.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const monitorStatusBarQueryString = ` -query MonitorStatus($dateRangeStart: String!, $dateRangeEnd: String!, $monitorId: String, $location: String) { - monitorStatus: getLatestMonitors( - dateRangeStart: $dateRangeStart - dateRangeEnd: $dateRangeEnd - monitorId: $monitorId - location: $location - ) { - timestamp - monitor { - status - duration { - us - } - } - observer { - geo { - name - } - } - tls { - certificate_not_valid_after - } - url { - full - } - } -} -`; - -export const monitorStatusBarQuery = gql` - ${monitorStatusBarQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx new file mode 100644 index 0000000000000..08d752f5b32ab --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { MonitorPage, OverviewPage, NotFoundPage } from './pages'; +import { AutocompleteProviderRegister } from '../../../../../src/plugins/data/public'; +import { UMUpdateBreadcrumbs } from './lib/lib'; + +export const MONITOR_ROUTE = '/monitor/:monitorId/:location?'; +export const OVERVIEW_ROUTE = '/'; + +interface RouterProps { + autocomplete: Pick; + basePath: string; + setBreadcrumbs: UMUpdateBreadcrumbs; +} + +export const PageRouter: FC = ({ autocomplete, basePath, setBreadcrumbs }) => ( + + + + + + + + + +); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap new file mode 100644 index 0000000000000..6fe2c8eaa362d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`overview filters action creators creates a fail action 1`] = ` +Object { + "payload": [Error: There was an error retrieving the overview filters], + "type": "FETCH_OVERVIEW_FILTERS_FAIL", +} +`; + +exports[`overview filters action creators creates a get action 1`] = ` +Object { + "payload": Object { + "dateRangeEnd": "now", + "dateRangeStart": "now-15m", + "locations": Array [ + "fairbanks", + "tokyo", + ], + "ports": Array [ + "80", + ], + "schemes": Array [ + "http", + "tcp", + ], + "search": "", + "statusFilter": "down", + "tags": Array [ + "api", + "dev", + ], + }, + "type": "FETCH_OVERVIEW_FILTERS", +} +`; + +exports[`overview filters action creators creates a success action 1`] = ` +Object { + "payload": Object { + "locations": Array [ + "fairbanks", + "tokyo", + "london", + ], + "ports": Array [ + 80, + 443, + ], + "schemes": Array [ + "http", + "tcp", + ], + "tags": Array [ + "api", + "dev", + "prod", + ], + }, + "type": "FETCH_OVERVIEW_FILTERS_SUCCESS", +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts b/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts new file mode 100644 index 0000000000000..4765e1327ce31 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.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 { + fetchOverviewFilters, + fetchOverviewFiltersSuccess, + fetchOverviewFiltersFail, +} from '../overview_filters'; + +describe('overview filters action creators', () => { + it('creates a get action', () => { + expect( + fetchOverviewFilters({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + statusFilter: 'down', + search: '', + locations: ['fairbanks', 'tokyo'], + ports: ['80'], + schemes: ['http', 'tcp'], + tags: ['api', 'dev'], + }) + ).toMatchSnapshot(); + }); + + it('creates a success action', () => { + expect( + fetchOverviewFiltersSuccess({ + locations: ['fairbanks', 'tokyo', 'london'], + ports: [80, 443], + schemes: ['http', 'tcp'], + tags: ['api', 'dev', 'prod'], + }) + ).toMatchSnapshot(); + }); + + it('creates a fail action', () => { + expect( + fetchOverviewFiltersFail(new Error('There was an error retrieving the overview filters')) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts index 6b896b07bb066..9874da1839c2f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts @@ -4,5 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './overview_filters'; export * from './snapshot'; export * from './ui'; +export * from './monitor_status'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts index 99855bb8c8df3..cf4525a08e43c 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MonitorDetailsActionPayload } from './types'; +import { MonitorError } from '../../../common/runtime_types'; import { MonitorLocations } from '../../../common/runtime_types'; import { QueryParams } from './types'; @@ -17,12 +19,12 @@ export const FETCH_MONITOR_LOCATIONS_FAIL = 'FETCH_MONITOR_LOCATIONS_FAIL'; export interface MonitorDetailsState { monitorId: string; - error: Error; + error: MonitorError; } interface GetMonitorDetailsAction { type: typeof FETCH_MONITOR_DETAILS; - payload: string; + payload: MonitorDetailsActionPayload; } interface GetMonitorDetailsSuccessAction { @@ -54,10 +56,10 @@ interface GetMonitorLocationsFailAction { payload: any; } -export function fetchMonitorDetails(monitorId: string): GetMonitorDetailsAction { +export function fetchMonitorDetails(payload: MonitorDetailsActionPayload): GetMonitorDetailsAction { return { type: FETCH_MONITOR_DETAILS, - payload: monitorId, + payload, }; } diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts new file mode 100644 index 0000000000000..db103f6cb780e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createAction } from 'redux-actions'; +import { QueryParams } from './types'; + +export const getSelectedMonitor = createAction<{ monitorId: string }>('GET_SELECTED_MONITOR'); +export const getSelectedMonitorSuccess = createAction('GET_SELECTED_MONITOR_SUCCESS'); +export const getSelectedMonitorFail = createAction('GET_SELECTED_MONITOR_FAIL'); + +export const getMonitorStatus = createAction('GET_MONITOR_STATUS'); +export const getMonitorStatusSuccess = createAction('GET_MONITOR_STATUS_SUCCESS'); +export const getMonitorStatusFail = createAction('GET_MONITOR_STATUS_FAIL'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts new file mode 100644 index 0000000000000..dbbd01e34b4d4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OverviewFilters } from '../../../common/runtime_types'; + +export const FETCH_OVERVIEW_FILTERS = 'FETCH_OVERVIEW_FILTERS'; +export const FETCH_OVERVIEW_FILTERS_FAIL = 'FETCH_OVERVIEW_FILTERS_FAIL'; +export const FETCH_OVERVIEW_FILTERS_SUCCESS = 'FETCH_OVERVIEW_FILTERS_SUCCESS'; + +export interface GetOverviewFiltersPayload { + dateRangeStart: string; + dateRangeEnd: string; + locations: string[]; + ports: string[]; + schemes: string[]; + search?: string; + statusFilter?: string; + tags: string[]; +} + +interface GetOverviewFiltersFetchAction { + type: typeof FETCH_OVERVIEW_FILTERS; + payload: GetOverviewFiltersPayload; +} + +interface GetOverviewFiltersSuccessAction { + type: typeof FETCH_OVERVIEW_FILTERS_SUCCESS; + payload: OverviewFilters; +} + +interface GetOverviewFiltersFailAction { + type: typeof FETCH_OVERVIEW_FILTERS_FAIL; + payload: Error; +} + +export type OverviewFiltersAction = + | GetOverviewFiltersFetchAction + | GetOverviewFiltersSuccessAction + | GetOverviewFiltersFailAction; + +export const fetchOverviewFilters = ( + payload: GetOverviewFiltersPayload +): GetOverviewFiltersFetchAction => ({ + type: FETCH_OVERVIEW_FILTERS, + payload, +}); + +export const fetchOverviewFiltersFail = (error: Error): GetOverviewFiltersFailAction => ({ + type: FETCH_OVERVIEW_FILTERS_FAIL, + payload: error, +}); + +export const fetchOverviewFiltersSuccess = ( + filters: OverviewFilters +): GetOverviewFiltersSuccessAction => ({ + type: FETCH_OVERVIEW_FILTERS_SUCCESS, + payload: filters, +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts index fe87a6a5960ee..57d2b4ce38204 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts @@ -5,6 +5,7 @@ */ import { Snapshot } from '../../../common/runtime_types'; + export const FETCH_SNAPSHOT_COUNT = 'FETCH_SNAPSHOT_COUNT'; export const FETCH_SNAPSHOT_COUNT_FAIL = 'FETCH_SNAPSHOT_COUNT_FAIL'; export const FETCH_SNAPSHOT_COUNT_SUCCESS = 'FETCH_SNAPSHOT_COUNT_SUCCESS'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts index 7ec288583f9fe..dba70ed839ac5 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts @@ -5,8 +5,17 @@ */ export interface QueryParams { + monitorId: string; dateStart: string; dateEnd: string; filters?: string; statusFilter?: string; + location?: string; +} + +export interface MonitorDetailsActionPayload { + monitorId: string; + dateStart: string; + dateEnd: string; + location?: string; } diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts index 0bb2d8447419b..d15d601737b2d 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts @@ -3,53 +3,21 @@ * 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 SET_INTEGRATION_POPOVER_STATE = 'SET_INTEGRATION_POPOVER_STATE'; -export const SET_BASE_PATH = 'SET_BASE_PATH'; -export const REFRESH_APP = 'REFRESH_APP'; +import { createAction } from 'redux-actions'; export interface PopoverState { id: string; open: boolean; } -interface SetBasePathAction { - type: typeof SET_BASE_PATH; - payload: string; -} +export type UiPayload = PopoverState & string & number & Map; -interface SetIntegrationPopoverAction { - type: typeof SET_INTEGRATION_POPOVER_STATE; - payload: PopoverState; -} +export const setBasePath = createAction('SET BASE PATH'); -interface TriggerAppRefreshAction { - type: typeof REFRESH_APP; - payload: number; -} +export const triggerAppRefresh = createAction('REFRESH APP'); -export type UiActionTypes = - | SetIntegrationPopoverAction - | SetBasePathAction - | TriggerAppRefreshAction; +export const setEsKueryString = createAction('SET ES KUERY STRING'); -export function toggleIntegrationsPopover(popoverState: PopoverState): SetIntegrationPopoverAction { - return { - type: SET_INTEGRATION_POPOVER_STATE, - payload: popoverState, - }; -} - -export function setBasePath(basePath: string): SetBasePathAction { - return { - type: SET_BASE_PATH, - payload: basePath, - }; -} - -export function triggerAppRefresh(refreshTime: number): TriggerAppRefreshAction { - return { - type: REFRESH_APP, - payload: refreshTime, - }; -} +export const toggleIntegrationsPopover = createAction( + 'TOGGLE INTEGRATION POPOVER STATE' +); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap index 53716681664c2..0d2392390c7e4 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,8 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`snapshot API throws when server response doesn't correspond to expected type 1`] = ` -[Error: Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/down: number -Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/mixed: number -Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/total: number -Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/up: number] +[Error: Invalid value undefined supplied to : { down: number, total: number, up: number }/down: number +Invalid value undefined supplied to : { down: number, total: number, up: number }/total: number +Invalid value undefined supplied to : { down: number, total: number, up: number }/up: number] `; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts index f5fdfb172bc58..e9b1391a23e32 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -14,7 +14,7 @@ describe('snapshot API', () => { fetchMock = jest.spyOn(window, 'fetch'); mockResponse = { ok: true, - json: () => new Promise(r => r({ up: 3, down: 12, mixed: 0, total: 15 })), + json: () => new Promise(r => r({ up: 3, down: 12, total: 15 })), }; }); @@ -34,7 +34,7 @@ describe('snapshot API', () => { expect(fetchMock).toHaveBeenCalledWith( '/api/uptime/snapshot/count?dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%22auto-http-0X21EE76EAC459873F%22&statusFilter=up' ); - expect(resp).toEqual({ up: 3, down: 12, mixed: 0, total: 15 }); + expect(resp).toEqual({ up: 3, down: 12, total: 15 }); }); it(`throws when server response doesn't correspond to expected type`, async () => { diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts index a4429868494f1..1d0cac5f87854 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -5,4 +5,6 @@ */ export * from './monitor'; +export * from './overview_filters'; export * from './snapshot'; +export * from './monitor_status'; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts index 0fb00b935342e..8b1220830f091 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts @@ -6,6 +6,7 @@ import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { getApiPath } from '../../lib/helper'; +import { BaseParams } from './types'; import { MonitorDetailsType, MonitorDetails, @@ -19,12 +20,23 @@ interface ApiRequest { basePath: string; } +export type MonitorQueryParams = BaseParams & ApiRequest; + export const fetchMonitorDetails = async ({ monitorId, basePath, -}: ApiRequest): Promise => { - const url = getApiPath(`/api/uptime/monitor/details?monitorId=${monitorId}`, basePath); - const response = await fetch(url); + dateStart, + dateEnd, +}: MonitorQueryParams): Promise => { + const url = getApiPath(`/api/uptime/monitor/details`, basePath); + const params = { + monitorId, + dateStart, + dateEnd, + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + if (!response.ok) { throw new Error(response.statusText); } diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts new file mode 100644 index 0000000000000..936e864b75619 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.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 { getApiPath } from '../../lib/helper'; +import { QueryParams } from '../actions/types'; +import { Ping } from '../../../common/graphql/types'; + +export interface APIParams { + basePath: string; + monitorId: string; +} + +export const fetchSelectedMonitor = async ({ basePath, monitorId }: APIParams): Promise => { + const url = getApiPath(`/api/uptime/monitor/selected`, basePath); + const params = { + monitorId, + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + return responseData; +}; + +export const fetchMonitorStatus = async ({ + basePath, + monitorId, + dateStart, + dateEnd, +}: QueryParams & APIParams): Promise => { + const url = getApiPath(`/api/uptime/monitor/status`, basePath); + const params = { + monitorId, + dateStart, + dateEnd, + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + return responseData; +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts new file mode 100644 index 0000000000000..c3ef62fa88dcf --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { GetOverviewFiltersPayload } from '../actions/overview_filters'; +import { getApiPath, parameterizeValues } from '../../lib/helper'; +import { OverviewFiltersType } from '../../../common/runtime_types'; + +type ApiRequest = GetOverviewFiltersPayload & { + basePath: string; +}; + +export const fetchOverviewFilters = async ({ + basePath, + dateRangeStart, + dateRangeEnd, + search, + schemes, + locations, + ports, + tags, +}: ApiRequest) => { + const url = getApiPath(`/api/uptime/filters`, basePath); + + const params = new URLSearchParams({ + dateRangeStart, + dateRangeEnd, + }); + + if (search) { + params.append('search', search); + } + + parameterizeValues(params, { schemes, locations, ports, tags }); + + const response = await fetch(`${url}?${params.toString()}`); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + const decoded = OverviewFiltersType.decode(responseData); + + ThrowReporter.report(decoded); + if (isRight(decoded)) { + return decoded.right; + } + throw new Error('`getOverviewFilters` response did not correspond to expected type'); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/types.ts b/x-pack/legacy/plugins/uptime/public/state/api/types.ts new file mode 100644 index 0000000000000..278cfce29986f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface BaseParams { + basePath: string; + dateStart: string; + dateEnd: string; + filters?: string; + statusFilter?: string; + location?: string; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts new file mode 100644 index 0000000000000..d293cdbe451b5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { call, put, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { getBasePath } from '../selectors'; + +/** + * Factory function for a fetch effect. It expects three action creators, + * one to call for a fetch, one to call for success, and one to handle failures. + * @param fetch creates a fetch action + * @param success creates a success action + * @param fail creates a failure action + * @template T the action type expected by the fetch action + * @template R the type that the API request should return on success + * @template S tye type of the success action + * @template F the type of the failure action + */ +export function fetchEffectFactory( + fetch: (request: T) => Promise, + success: (response: R) => Action, + fail: (error: Error) => Action +) { + return function*(action: Action) { + try { + if (!action.payload) { + yield put(fail(new Error('Cannot fetch snapshot for undefined parameters.'))); + return; + } + const { + payload: { ...params }, + } = action; + const basePath = yield select(getBasePath); + const response = yield call(fetch, { ...params, basePath }); + yield put(success(response)); + } catch (error) { + yield put(fail(error)); + } + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts index 4eb027d642974..41dda145edb4e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -6,9 +6,13 @@ import { fork } from 'redux-saga/effects'; import { fetchMonitorDetailsEffect } from './monitor'; -import { fetchSnapshotCountSaga } from './snapshot'; +import { fetchOverviewFiltersEffect } from './overview_filters'; +import { fetchSnapshotCountEffect } from './snapshot'; +import { fetchMonitorStatusEffect } from './monitor_status'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); - yield fork(fetchSnapshotCountSaga); + yield fork(fetchSnapshotCountEffect); + yield fork(fetchOverviewFiltersEffect); + yield fork(fetchMonitorStatusEffect); } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts index 210004bb343bb..1cac7424b4e5b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts @@ -16,12 +16,18 @@ import { } from '../actions/monitor'; import { fetchMonitorDetails, fetchMonitorLocations } from '../api'; import { getBasePath } from '../selectors'; +import { MonitorDetailsActionPayload } from '../actions/types'; function* monitorDetailsEffect(action: Action) { - const monitorId: string = action.payload; + const { monitorId, dateStart, dateEnd }: MonitorDetailsActionPayload = action.payload; try { const basePath = yield select(getBasePath); - const response = yield call(fetchMonitorDetails, { monitorId, basePath }); + const response = yield call(fetchMonitorDetails, { + monitorId, + basePath, + dateStart, + dateEnd, + }); yield put({ type: FETCH_MONITOR_DETAILS_SUCCESS, payload: response }); } catch (error) { yield put({ type: FETCH_MONITOR_DETAILS_FAIL, payload: error.message }); diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts new file mode 100644 index 0000000000000..cab32092a14cd --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { call, put, takeLatest, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { + getSelectedMonitor, + getSelectedMonitorSuccess, + getSelectedMonitorFail, + getMonitorStatus, + getMonitorStatusSuccess, + getMonitorStatusFail, +} from '../actions/monitor_status'; +import { fetchSelectedMonitor, fetchMonitorStatus } from '../api'; +import { getBasePath } from '../selectors'; + +function* selectedMonitorEffect(action: Action) { + const { monitorId } = action.payload; + try { + const basePath = yield select(getBasePath); + const response = yield call(fetchSelectedMonitor, { + monitorId, + basePath, + }); + yield put({ type: getSelectedMonitorSuccess, payload: response }); + } catch (error) { + yield put({ type: getSelectedMonitorFail, payload: error.message }); + } +} + +function* monitorStatusEffect(action: Action) { + const { monitorId, dateStart, dateEnd } = action.payload; + try { + const basePath = yield select(getBasePath); + const response = yield call(fetchMonitorStatus, { + monitorId, + basePath, + dateStart, + dateEnd, + }); + yield put({ type: getMonitorStatusSuccess, payload: response }); + } catch (error) { + yield put({ type: getMonitorStatusFail, payload: error.message }); + } +} + +export function* fetchMonitorStatusEffect() { + yield takeLatest(getMonitorStatus, monitorStatusEffect); + yield takeLatest(getSelectedMonitor, selectedMonitorEffect); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts new file mode 100644 index 0000000000000..92b578bafed2d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { takeLatest } from 'redux-saga/effects'; +import { + FETCH_OVERVIEW_FILTERS, + fetchOverviewFiltersFail, + fetchOverviewFiltersSuccess, +} from '../actions'; +import { fetchOverviewFilters } from '../api'; +import { fetchEffectFactory } from './fetch_effect'; + +export function* fetchOverviewFiltersEffect() { + yield takeLatest( + FETCH_OVERVIEW_FILTERS, + fetchEffectFactory(fetchOverviewFilters, fetchOverviewFiltersSuccess, fetchOverviewFiltersFail) + ); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts index 23ac1016d2244..91df43dd9e826 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts @@ -4,42 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { call, put, takeLatest, select } from 'redux-saga/effects'; -import { Action } from 'redux-actions'; +import { takeLatest } from 'redux-saga/effects'; import { FETCH_SNAPSHOT_COUNT, - GetSnapshotPayload, fetchSnapshotCountFail, fetchSnapshotCountSuccess, } from '../actions'; import { fetchSnapshotCount } from '../api'; -import { getBasePath } from '../selectors'; +import { fetchEffectFactory } from './fetch_effect'; -function* snapshotSaga(action: Action) { - try { - if (!action.payload) { - yield put( - fetchSnapshotCountFail(new Error('Cannot fetch snapshot for undefined parameters.')) - ); - return; - } - const { - payload: { dateRangeStart, dateRangeEnd, filters, statusFilter }, - } = action; - const basePath = yield select(getBasePath); - const response = yield call(fetchSnapshotCount, { - basePath, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, - }); - yield put(fetchSnapshotCountSuccess(response)); - } catch (error) { - yield put(fetchSnapshotCountFail(error)); - } -} - -export function* fetchSnapshotCountSaga() { - yield takeLatest(FETCH_SNAPSHOT_COUNT, snapshotSaga); +export function* fetchSnapshotCountEffect() { + yield takeLatest( + FETCH_SNAPSHOT_COUNT, + fetchEffectFactory(fetchSnapshotCount, fetchSnapshotCountSuccess, fetchSnapshotCountFail) + ); } diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap index d3a21ec9eece3..7a3c72f93d86f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap @@ -4,7 +4,6 @@ exports[`snapshot reducer appends a current error to existing errors list 1`] = Object { "count": Object { "down": 0, - "mixed": 0, "total": 0, "up": 0, }, @@ -19,7 +18,6 @@ exports[`snapshot reducer changes the count when a snapshot fetch succeeds 1`] = Object { "count": Object { "down": 15, - "mixed": 0, "total": 25, "up": 10, }, @@ -32,7 +30,6 @@ exports[`snapshot reducer sets the state's status to loading during a fetch 1`] Object { "count": Object { "down": 0, - "mixed": 0, "total": 0, "up": 0, }, @@ -45,7 +42,6 @@ exports[`snapshot reducer updates existing state 1`] = ` Object { "count": Object { "down": 1, - "mixed": 0, "total": 4, "up": 3, }, diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap index 75516da18c633..5d03c0058c3c1 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap @@ -3,6 +3,7 @@ exports[`ui reducer adds integration popover status to state 1`] = ` Object { "basePath": "", + "esKuery": "", "integrationsPopoverOpen": Object { "id": "popover-2", "open": true, @@ -14,6 +15,7 @@ Object { exports[`ui reducer sets the application's base path 1`] = ` Object { "basePath": "yyz", + "esKuery": "", "integrationsPopoverOpen": null, "lastRefresh": 125, } @@ -21,7 +23,8 @@ Object { exports[`ui reducer updates the refresh value 1`] = ` Object { - "basePath": "", + "basePath": "abc", + "esKuery": "", "integrationsPopoverOpen": null, "lastRefresh": 125, } diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts index a4b317d5af197..95c576e0fd72e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts @@ -21,7 +21,7 @@ describe('snapshot reducer', () => { expect( snapshotReducer( { - count: { down: 1, mixed: 0, total: 4, up: 3 }, + count: { down: 1, total: 4, up: 3 }, errors: [], loading: false, }, @@ -47,7 +47,6 @@ describe('snapshot reducer', () => { payload: { up: 10, down: 15, - mixed: 0, total: 25, }, }; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts index 9be863f0b700d..417095b64ba2d 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UiActionTypes } from '../../actions'; +import { setBasePath, toggleIntegrationsPopover, triggerAppRefresh } from '../../actions'; import { uiReducer } from '../ui'; +import { Action } from 'redux-actions'; describe('ui reducer', () => { it(`sets the application's base path`, () => { - const action: UiActionTypes = { - type: 'SET_BASE_PATH', - payload: 'yyz', - }; + const action = setBasePath('yyz') as Action; expect( uiReducer( { basePath: 'abc', + esKuery: '', integrationsPopoverOpen: null, lastRefresh: 125, }, @@ -26,17 +25,15 @@ describe('ui reducer', () => { }); it('adds integration popover status to state', () => { - const action: UiActionTypes = { - type: 'SET_INTEGRATION_POPOVER_STATE', - payload: { - id: 'popover-2', - open: true, - }, - }; + const action = toggleIntegrationsPopover({ + id: 'popover-2', + open: true, + }) as Action; expect( uiReducer( { basePath: '', + esKuery: '', integrationsPopoverOpen: null, lastRefresh: 125, }, @@ -46,10 +43,17 @@ describe('ui reducer', () => { }); it('updates the refresh value', () => { - const action: UiActionTypes = { - type: 'REFRESH_APP', - payload: 125, - }; - expect(uiReducer(undefined, action)).toMatchSnapshot(); + const action = triggerAppRefresh(125) as Action; + expect( + uiReducer( + { + basePath: 'abc', + esKuery: '', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index f0c3d1c2cbecf..5f915d970e543 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -6,11 +6,15 @@ import { combineReducers } from 'redux'; import { monitorReducer } from './monitor'; +import { overviewFiltersReducer } from './overview_filters'; import { snapshotReducer } from './snapshot'; import { uiReducer } from './ui'; +import { monitorStatusReducer } from './monitor_status'; export const rootReducer = combineReducers({ monitor: monitorReducer, + overviewFilters: overviewFiltersReducer, snapshot: snapshotReducer, ui: uiReducer, + monitorStatus: monitorStatusReducer, }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts index 220ab0b205462..aac8a90598d0c 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts @@ -19,10 +19,10 @@ import { MonitorLocations } from '../../../common/runtime_types'; type MonitorLocationsList = Map; export interface MonitorState { - monitorDetailsList: MonitorDetailsState[]; - monitorLocationsList: MonitorLocationsList; loading: boolean; errors: any[]; + monitorDetailsList: MonitorDetailsState[]; + monitorLocationsList: MonitorLocationsList; } const initialState: MonitorState = { diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts new file mode 100644 index 0000000000000..2688a0946dd61 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { handleActions, Action } from 'redux-actions'; +import { + getSelectedMonitor, + getSelectedMonitorSuccess, + getSelectedMonitorFail, + getMonitorStatus, + getMonitorStatusSuccess, + getMonitorStatusFail, +} from '../actions'; +import { Ping } from '../../../common/graphql/types'; +import { QueryParams } from '../actions/types'; + +export interface MonitorStatusState { + status: Ping | null; + monitor: Ping | null; + loading: boolean; +} + +const initialState: MonitorStatusState = { + status: null, + monitor: null, + loading: false, +}; + +type MonitorStatusPayload = QueryParams & Ping; + +export const monitorStatusReducer = handleActions( + { + [String(getSelectedMonitor)]: (state, action: Action) => ({ + ...state, + loading: true, + }), + + [String(getSelectedMonitorSuccess)]: (state, action: Action) => ({ + ...state, + loading: false, + monitor: { ...action.payload } as Ping, + }), + + [String(getSelectedMonitorFail)]: (state, action: Action) => ({ + ...state, + loading: false, + }), + + [String(getMonitorStatus)]: (state, action: Action) => ({ + ...state, + loading: true, + }), + + [String(getMonitorStatusSuccess)]: (state, action: Action) => ({ + ...state, + loading: false, + status: { ...action.payload } as Ping, + }), + + [String(getMonitorStatusFail)]: (state, action: Action) => ({ + ...state, + loading: false, + }), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts new file mode 100644 index 0000000000000..b219421f4f4dc --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OverviewFilters } from '../../../common/runtime_types'; +import { + FETCH_OVERVIEW_FILTERS, + FETCH_OVERVIEW_FILTERS_FAIL, + FETCH_OVERVIEW_FILTERS_SUCCESS, + OverviewFiltersAction, +} from '../actions'; + +export interface OverviewFiltersState { + filters: OverviewFilters; + errors: Error[]; + loading: boolean; +} + +const initialState: OverviewFiltersState = { + filters: { + locations: [], + ports: [], + schemes: [], + tags: [], + }, + errors: [], + loading: false, +}; + +export function overviewFiltersReducer( + state = initialState, + action: OverviewFiltersAction +): OverviewFiltersState { + switch (action.type) { + case FETCH_OVERVIEW_FILTERS: + return { + ...state, + loading: true, + }; + case FETCH_OVERVIEW_FILTERS_SUCCESS: + return { + ...state, + filters: action.payload, + loading: false, + }; + case FETCH_OVERVIEW_FILTERS_FAIL: + return { + ...state, + errors: [...state.errors, action.payload], + }; + default: + return state; + } +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts index dd9449325f4fb..2155d0e3a74e3 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts @@ -21,7 +21,6 @@ export interface SnapshotState { const initialState: SnapshotState = { count: { down: 0, - mixed: 0, total: 0, up: 0, }, diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts index be95c8fff6bec..bb5bd22085ac6 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts @@ -4,49 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ +import { handleActions, Action } from 'redux-actions'; import { - UiActionTypes, PopoverState, - SET_INTEGRATION_POPOVER_STATE, - SET_BASE_PATH, - REFRESH_APP, + toggleIntegrationsPopover, + setBasePath, + setEsKueryString, + triggerAppRefresh, + UiPayload, } from '../actions/ui'; export interface UiState { integrationsPopoverOpen: PopoverState | null; basePath: string; + esKuery: string; lastRefresh: number; } const initialState: UiState = { integrationsPopoverOpen: null, basePath: '', + esKuery: '', lastRefresh: Date.now(), }; -export function uiReducer(state = initialState, action: UiActionTypes): UiState { - switch (action.type) { - case REFRESH_APP: - return { - ...state, - lastRefresh: action.payload, - }; - case SET_INTEGRATION_POPOVER_STATE: - const popoverState = action.payload; - return { - ...state, - integrationsPopoverOpen: { - id: popoverState.id, - open: popoverState.open, - }, - }; - case SET_BASE_PATH: - const basePath = action.payload; - return { - ...state, - basePath, - }; - default: - return state; - } -} +export const uiReducer = handleActions( + { + [String(toggleIntegrationsPopover)]: (state, action: Action) => ({ + ...state, + integrationsPopoverOpen: action.payload as PopoverState, + }), + + [String(setBasePath)]: (state, action: Action) => ({ + ...state, + basePath: action.payload as string, + }), + + [String(triggerAppRefresh)]: (state, action: Action) => ({ + ...state, + lastRefresh: action.payload as number, + }), + + [String(setEsKueryString)]: (state, action: Action) => ({ + ...state, + esKuery: action.payload as string, + }), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index b61ed83663435..38fb3edea4768 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -9,6 +9,16 @@ import { AppState } from '../../../state'; describe('state selectors', () => { const state: AppState = { + overviewFilters: { + filters: { + locations: [], + ports: [], + schemes: [], + tags: [], + }, + errors: [], + loading: false, + }, monitor: { monitorDetailsList: [], monitorLocationsList: new Map(), @@ -19,13 +29,22 @@ describe('state selectors', () => { count: { up: 2, down: 0, - mixed: 0, total: 2, }, errors: [], loading: false, }, - ui: { basePath: 'yyz', integrationsPopoverOpen: null, lastRefresh: 125 }, + ui: { + basePath: 'yyz', + esKuery: '', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + monitorStatus: { + status: null, + monitor: null, + loading: false, + }, }; it('selects base path from state', () => { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 1792c84c45220..337e99f6ede16 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -6,15 +6,25 @@ import { AppState } from '../../state'; +// UI Selectors export const getBasePath = ({ ui: { basePath } }: AppState) => basePath; export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: AppState) => integrationsPopoverOpen; +// Monitor Selectors export const getMonitorDetails = (state: AppState, summary: any) => { return state.monitor.monitorDetailsList[summary.monitor_id]; }; -export const getMonitorLocations = (state: AppState, monitorId: string) => { +export const selectMonitorLocations = (state: AppState, monitorId: string) => { return state.monitor.monitorLocationsList?.get(monitorId); }; + +export const selectSelectedMonitor = (state: AppState) => { + return state.monitorStatus.monitor; +}; + +export const selectMonitorStatus = (state: AppState) => { + return state.monitorStatus.status; +}; diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index f72055c52255d..25ff0e7177016 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -5,24 +5,25 @@ */ import DateMath from '@elastic/datemath'; -import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPage } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { ApolloProvider } from 'react-apollo'; import { Provider as ReduxProvider } from 'react-redux'; -import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { I18nStart, ChromeBreadcrumb } from 'src/core/public'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; +import { BrowserRouter as Router, Route, RouteComponentProps } from 'react-router-dom'; +import { I18nStart, ChromeBreadcrumb, LegacyCoreStart } from 'src/core/public'; +import { PluginsStart } from 'ui/new_platform/new_platform'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib'; -import { MonitorPage, OverviewPage, NotFoundPage } from './pages'; import { UptimeRefreshContext, UptimeSettingsContext, UMSettingsContextValues } from './contexts'; -import { UptimeDatePicker, CommonlyUsedRange } from './components/functional/uptime_date_picker'; +import { CommonlyUsedRange } from './components/functional/uptime_date_picker'; import { useUrlParams } from './hooks'; import { getTitle } from './lib/helper/get_title'; import { store } from './state'; import { setBasePath, triggerAppRefresh } from './state/actions'; +import { PageRouter } from './routes'; export interface UptimeAppColors { danger: string; @@ -37,15 +38,14 @@ export interface UptimeAppProps { basePath: string; canSave: boolean; client: UMGraphQLClient; + core: LegacyCoreStart; darkMode: boolean; - autocomplete: Pick; i18n: I18nStart; isApmAvailable: boolean; isInfraAvailable: boolean; isLogsAvailable: boolean; kibanaBreadcrumbs: ChromeBreadcrumb[]; - logMonitorPageLoad: () => void; - logOverviewPageLoad: () => void; + plugins: PluginsStart; routerBasename: string; setBreadcrumbs: UMUpdateBreadcrumbs; setBadge: UMUpdateBadge; @@ -55,18 +55,17 @@ export interface UptimeAppProps { const Application = (props: UptimeAppProps) => { const { - autocomplete, basePath, canSave, client, + core, darkMode, commonlyUsedRanges, i18n: i18nCore, isApmAvailable, isInfraAvailable, isLogsAvailable, - logMonitorPageLoad, - logOverviewPageLoad, + plugins, renderGlobalHelpControls, routerBasename, setBreadcrumbs, @@ -94,7 +93,6 @@ const Application = (props: UptimeAppProps) => { }; } const [lastRefresh, setLastRefresh] = useState(Date.now()); - const [headingText, setHeadingText] = useState(undefined); useEffect(() => { renderGlobalHelpControls(); @@ -147,7 +145,7 @@ const Application = (props: UptimeAppProps) => { isInfraAvailable, isLogsAvailable, refreshApp, - setHeadingText, + commonlyUsedRanges, }; }; @@ -156,70 +154,32 @@ const Application = (props: UptimeAppProps) => { return ( - - { - return ( - - - - -
- - - -

{headingText}

-
-
- - - -
- - - ( - - )} + + + { + return ( + + + + +
+ - ( - - )} - /> - - -
-
-
-
-
- ); - }} - /> -
+
+
+
+
+
+ ); + }} + /> +
+
); diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts index 8b685d8e08a2b..897d67dde807e 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -7,14 +7,9 @@ import { UMGqlRange } from '../../../common/domain_types'; import { UMResolver } from '../../../common/graphql/resolver_types'; import { - FilterBar, GetFilterBarQueryArgs, - GetLatestMonitorsQueryArgs, GetMonitorChartsDataQueryArgs, - GetMonitorPageTitleQueryArgs, MonitorChart, - MonitorPageTitle, - Ping, GetSnapshotHistogramQueryArgs, } from '../../../common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; @@ -23,13 +18,6 @@ import { HistogramResult } from '../../../common/domain_types'; export type UMMonitorsResolver = UMResolver, any, UMGqlRange, UMContext>; -export type UMLatestMonitorsResolver = UMResolver< - Ping[] | Promise, - any, - GetLatestMonitorsQueryArgs, - UMContext ->; - export type UMGetMonitorChartsResolver = UMResolver< any | Promise, any, @@ -44,13 +32,6 @@ export type UMGetFilterBarResolver = UMResolver< UMContext >; -export type UMGetMontiorPageTitleResolver = UMResolver< - MonitorPageTitle | Promise | null, - any, - GetMonitorPageTitleQueryArgs, - UMContext ->; - export type UMGetSnapshotHistogram = UMResolver< HistogramResult | Promise, any, @@ -64,9 +45,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( Query: { getSnapshotHistogram: UMGetSnapshotHistogram; getMonitorChartsData: UMGetMonitorChartsResolver; - getLatestMonitors: UMLatestMonitorsResolver; - getFilterBar: UMGetFilterBarResolver; - getMonitorPageTitle: UMGetMontiorPageTitleResolver; }; } => ({ Query: { @@ -97,36 +75,5 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( location, }); }, - async getLatestMonitors( - _resolver, - { dateRangeStart, dateRangeEnd, monitorId, location }, - { APICaller } - ): Promise { - return await libs.pings.getLatestMonitorDocs({ - callES: APICaller, - dateRangeStart, - dateRangeEnd, - monitorId, - location, - }); - }, - async getFilterBar( - _resolver, - { dateRangeStart, dateRangeEnd }, - { APICaller } - ): Promise { - return await libs.monitors.getFilterBar({ - callES: APICaller, - dateRangeStart, - dateRangeEnd, - }); - }, - async getMonitorPageTitle( - _resolver: any, - { monitorId }, - { APICaller } - ): Promise { - return await libs.monitors.getMonitorPageTitle({ callES: APICaller, monitorId }); - }, }, }); diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts index f9b14c63e70bb..8a86d97b4cd8e 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts @@ -7,22 +7,6 @@ import gql from 'graphql-tag'; export const monitorsSchema = gql` - "The data used to enrich the filter bar." - type FilterBar { - "A series of monitor IDs in the heartbeat indices." - ids: [String!] - "The location values users have configured for the agents." - locations: [String!] - "The ports of the monitored endpoints." - ports: [Int!] - "The schemes used by the monitors." - schemes: [String!] - "The possible status values contained in the indices." - statuses: [String!] - "The list of URLs" - urls: [String!] - } - type HistogramDataPoint { upCount: Int downCount: Int @@ -114,12 +98,6 @@ export const monitorsSchema = gql` interval: UnsignedInteger! } - type MonitorPageTitle { - id: String! - url: String - name: String - } - extend type Query { getMonitors( dateRangeStart: String! @@ -142,21 +120,5 @@ export const monitorsSchema = gql` dateRangeEnd: String! location: String ): MonitorChart - - "Fetch the most recent event data for a monitor ID, date range, location." - getLatestMonitors( - "The lower limit of the date range." - dateRangeStart: String! - "The upper limit of the date range." - dateRangeEnd: String! - "Optional: a specific monitor ID filter." - monitorId: String - "Optional: a specific instance location filter." - location: String - ): [Ping!]! - - getFilterBar(dateRangeStart: String!, dateRangeEnd: String!): FilterBar - - getMonitorPageTitle(monitorId: String!): MonitorPageTitle } `; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap deleted file mode 100644 index 29c82ff455d36..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`get snapshot helper reduces check groups as expected 1`] = ` -Object { - "down": 1, - "mixed": 0, - "total": 3, - "up": 2, -} -`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts deleted file mode 100644 index 917e4a149de67..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts +++ /dev/null @@ -1,106 +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 { getSnapshotCountHelper } from '../get_snapshot_helper'; -import { MonitorGroups } from '../search'; - -describe('get snapshot helper', () => { - let mockIterator: any; - beforeAll(() => { - mockIterator = jest.fn(); - const summaryTimestamp = new Date('2019-01-01'); - const firstResult: MonitorGroups = { - id: 'firstGroup', - groups: [ - { - monitorId: 'first-monitor', - location: 'us-east-1', - checkGroup: 'abc', - status: 'down', - summaryTimestamp, - }, - { - monitorId: 'first-monitor', - location: 'us-west-1', - checkGroup: 'abc', - status: 'up', - summaryTimestamp, - }, - { - monitorId: 'first-monitor', - location: 'amsterdam', - checkGroup: 'abc', - status: 'down', - summaryTimestamp, - }, - ], - }; - const secondResult: MonitorGroups = { - id: 'secondGroup', - groups: [ - { - monitorId: 'second-monitor', - location: 'us-east-1', - checkGroup: 'yyz', - status: 'up', - summaryTimestamp, - }, - { - monitorId: 'second-monitor', - location: 'us-west-1', - checkGroup: 'yyz', - status: 'up', - summaryTimestamp, - }, - { - monitorId: 'second-monitor', - location: 'amsterdam', - checkGroup: 'yyz', - status: 'up', - summaryTimestamp, - }, - ], - }; - const thirdResult: MonitorGroups = { - id: 'thirdGroup', - groups: [ - { - monitorId: 'third-monitor', - location: 'us-east-1', - checkGroup: 'dt', - status: 'up', - summaryTimestamp, - }, - { - monitorId: 'third-monitor', - location: 'us-west-1', - checkGroup: 'dt', - status: 'up', - summaryTimestamp, - }, - { - monitorId: 'third-monitor', - location: 'amsterdam', - checkGroup: 'dt', - status: 'up', - summaryTimestamp, - }, - ], - }; - - const mockNext = jest - .fn() - .mockReturnValueOnce(firstResult) - .mockReturnValueOnce(secondResult) - .mockReturnValueOnce(thirdResult) - .mockReturnValueOnce(null); - mockIterator.next = mockNext; - }); - - it('reduces check groups as expected', async () => { - expect(await getSnapshotCountHelper(mockIterator)).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts index d264da2e7ec0c..eaaa8087e57cd 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts @@ -4,22 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMMonitorStatesAdapter, CursorPagination } from './adapter_types'; +import { UMMonitorStatesAdapter } from './adapter_types'; import { INDEX_NAMES, CONTEXT_DEFAULTS } from '../../../../common/constants'; import { fetchPage } from './search'; import { MonitorGroupIterator } from './search/monitor_group_iterator'; -import { getSnapshotCountHelper } from './get_snapshot_helper'; - -export interface QueryContext { - count: (query: Record) => Promise; - search: (query: Record) => Promise; - dateRangeStart: string; - dateRangeEnd: string; - pagination: CursorPagination; - filterClause: any | null; - size: number; - statusFilter?: string; -} +import { Snapshot } from '../../../../common/runtime_types'; +import { QueryContext } from './search/query_context'; export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = { // Gets a page of monitor states. @@ -35,16 +25,15 @@ export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = { statusFilter = statusFilter === null ? undefined : statusFilter; const size = 10; - const queryContext: QueryContext = { - count: (query: Record): Promise => callES('count', query), - search: (query: Record): Promise => callES('search', query), + const queryContext = new QueryContext( + callES, dateRangeStart, dateRangeEnd, pagination, - filterClause: filters && filters !== '' ? JSON.parse(filters) : null, + filters && filters !== '' ? JSON.parse(filters) : null, size, - statusFilter, - }; + statusFilter + ); const page = await fetchPage(queryContext); @@ -55,18 +44,46 @@ export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = { }; }, - getSnapshotCount: async ({ callES, dateRangeStart, dateRangeEnd, filters, statusFilter }) => { - const context: QueryContext = { - count: query => callES('count', query), - search: query => callES('search', query), + getSnapshotCount: async ({ + callES, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }): Promise => { + if (!(statusFilter === 'up' || statusFilter === 'down' || statusFilter === undefined)) { + throw new Error(`Invalid status filter value '${statusFilter}'`); + } + + const context = new QueryContext( + callES, dateRangeStart, dateRangeEnd, - pagination: CONTEXT_DEFAULTS.CURSOR_PAGINATION, - filterClause: filters && filters !== '' ? JSON.parse(filters) : null, - size: CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT, - statusFilter, + CONTEXT_DEFAULTS.CURSOR_PAGINATION, + filters && filters !== '' ? JSON.parse(filters) : null, + CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT, + statusFilter + ); + + // Calculate the total, up, and down counts. + const counts = await fastStatusCount(context); + + // Check if the last count was accurate, if not, we need to perform a slower count with the + // MonitorGroupsIterator. + if (!(await context.hasTimespan())) { + // Figure out whether 'up' or 'down' is more common. It's faster to count the lower cardinality + // one then use subtraction to figure out its opposite. + const [leastCommonStatus, mostCommonStatus]: Array<'up' | 'down'> = + counts.up > counts.down ? ['down', 'up'] : ['up', 'down']; + counts[leastCommonStatus] = await slowStatusCount(context, leastCommonStatus); + counts[mostCommonStatus] = counts.total - counts[leastCommonStatus]; + } + + return { + total: statusFilter ? counts[statusFilter] : counts.total, + up: statusFilter === 'down' ? 0 : counts.up, + down: statusFilter === 'up' ? 0 : counts.down, }; - return getSnapshotCountHelper(new MonitorGroupIterator(context)); }, statesIndexExists: async ({ callES }) => { @@ -92,3 +109,46 @@ const jsonifyPagination = (p: any): string | null => { return JSON.stringify(p); }; + +const fastStatusCount = async (context: QueryContext): Promise => { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 0, + query: { bool: { filter: await context.dateAndCustomFilters() } }, + aggs: { + unique: { + // We set the precision threshold to 40k which is the max precision supported by cardinality + cardinality: { field: 'monitor.id', precision_threshold: 40000 }, + }, + down: { + filter: { range: { 'summary.down': { gt: 0 } } }, + aggs: { + unique: { cardinality: { field: 'monitor.id', precision_threshold: 40000 } }, + }, + }, + }, + }, + }; + + const statistics = await context.search(params); + const total = statistics.aggregations.unique.value; + const down = statistics.aggregations.down.unique.value; + + return { + total, + down, + up: total - down, + }; +}; + +const slowStatusCount = async (context: QueryContext, status: string): Promise => { + const downContext = context.clone(); + downContext.statusFilter = status; + const iterator = new MonitorGroupIterator(downContext); + let count = 0; + while (await iterator.next()) { + count++; + } + return count; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts deleted file mode 100644 index 8bd21b77406df..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MonitorGroups, MonitorGroupIterator } from './search'; -import { Snapshot } from '../../../../common/runtime_types'; - -const reduceItemsToCounts = (items: MonitorGroups[]) => { - let down = 0; - let up = 0; - items.forEach(item => { - if (item.groups.some(group => group.status === 'down')) { - down++; - } else { - up++; - } - }); - return { - down, - mixed: 0, - total: down + up, - up, - }; -}; - -export const getSnapshotCountHelper = async (iterator: MonitorGroupIterator): Promise => { - const items: MonitorGroups[] = []; - let res: MonitorGroups | null; - // query the index to find the most recent check group for each monitor/location - do { - res = await iterator.next(); - if (res) { - items.push(res); - } - } while (res !== null); - - return reduceItemsToCounts(items); -}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts index d571a5a902539..0bbdaa87a5e66 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts @@ -11,7 +11,7 @@ import { MonitorGroupsFetcher, MonitorGroupsPage, } from '../fetch_page'; -import { QueryContext } from '../../elasticsearch_monitor_states_adapter'; +import { QueryContext } from '../query_context'; import { MonitorSummary } from '../../../../../../common/graphql/types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/monitor_group_iterator.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/monitor_group_iterator.test.ts index b8df3b635dc6b..0ce5e75195475 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/monitor_group_iterator.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/monitor_group_iterator.test.ts @@ -11,8 +11,8 @@ import { MonitorGroupIterator, } from '../monitor_group_iterator'; import { simpleQueryContext } from './test_helpers'; -import { QueryContext } from '../../elasticsearch_monitor_states_adapter'; import { MonitorGroups } from '../fetch_page'; +import { QueryContext } from '../query_context'; describe('iteration', () => { let iterator: MonitorGroupIterator | null = null; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts index d6fe5f82e735d..bb3f3da3e289d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts @@ -6,7 +6,7 @@ import { CursorPagination } from '../../adapter_types'; import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; -import { QueryContext } from '../../elasticsearch_monitor_states_adapter'; +import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { return { @@ -23,14 +23,5 @@ export const nextPagination = (key: any): CursorPagination => { }; }; export const simpleQueryContext = (): QueryContext => { - return { - count: _query => new Promise(r => ({})), - search: _query => new Promise(r => ({})), - dateRangeEnd: '', - dateRangeStart: '', - filterClause: undefined, - pagination: nextPagination('something'), - size: 0, - statusFilter: '', - }; + return new QueryContext(undefined, '', '', nextPagination('something'), undefined, 0, ''); }; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts index 093e105635c2c..b64015424ff40 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts @@ -5,7 +5,7 @@ */ import { get, sortBy } from 'lodash'; -import { QueryContext } from '../elasticsearch_monitor_states_adapter'; +import { QueryContext } from './query_context'; import { getHistogramIntervalFormatted } from '../../../helper'; import { INDEX_NAMES, STATES } from '../../../../../common/constants'; import { @@ -77,19 +77,19 @@ export const enrichMonitorGroups: MonitorEnricher = async ( } String agentIdIP = agentId + "-" + (ip == null ? "" : ip.toString()); def ts = doc["@timestamp"][0].toInstant().toEpochMilli(); - + def lastCheck = state.checksByAgentIdIP[agentId]; Instant lastTs = lastCheck != null ? lastCheck["@timestamp"] : null; if (lastTs != null && lastTs > ts) { return; } - + curCheck.put("@timestamp", ts); - + Map agent = new HashMap(); agent.id = agentId; curCheck.put("agent", agent); - + if (state.globals.url == null) { Map url = new HashMap(); Collection fields = ["full", "original", "scheme", "username", "password", "domain", "port", "path", "query", "fragment"]; @@ -102,7 +102,7 @@ export const enrichMonitorGroups: MonitorEnricher = async ( } state.globals.url = url; } - + Map monitor = new HashMap(); monitor.status = doc["monitor.status"][0]; monitor.ip = ip; @@ -113,7 +113,7 @@ export const enrichMonitorGroups: MonitorEnricher = async ( } } curCheck.monitor = monitor; - + if (curCheck.observer == null) { curCheck.observer = new HashMap(); } @@ -144,14 +144,14 @@ export const enrichMonitorGroups: MonitorEnricher = async ( if (!doc["tls.certificate_not_valid_before"].isEmpty()) { curCheck.tls.certificate_not_valid_before = doc["tls.certificate_not_valid_before"][0]; } - + state.checksByAgentIdIP[agentIdIP] = curCheck; `, combine_script: 'return state;', reduce_script: ` // The final document Map result = new HashMap(); - + Map checks = new HashMap(); Instant maxTs = Instant.ofEpochMilli(0); Collection ips = new HashSet(); @@ -159,7 +159,7 @@ export const enrichMonitorGroups: MonitorEnricher = async ( Collection podUids = new HashSet(); Collection containerIds = new HashSet(); Collection tls = new HashSet(); - String name = null; + String name = null; for (state in states) { result.putAll(state.globals); for (entry in state.checksByAgentIdIP.entrySet()) { @@ -167,18 +167,18 @@ export const enrichMonitorGroups: MonitorEnricher = async ( def check = entry.getValue(); def lastBestCheck = checks.get(agentIdIP); def checkTs = Instant.ofEpochMilli(check.get("@timestamp")); - + if (maxTs.isBefore(checkTs)) { maxTs = checkTs} - + if (lastBestCheck == null || lastBestCheck.get("@timestamp") < checkTs) { check["@timestamp"] = check["@timestamp"]; checks[agentIdIP] = check } - + if (check.monitor.name != null && check.monitor.name != "") { name = check.monitor.name; } - + ips.add(check.monitor.ip); if (check.observer != null && check.observer.geo != null && check.observer.geo.name != null) { geoNames.add(check.observer.geo.name); @@ -194,45 +194,45 @@ export const enrichMonitorGroups: MonitorEnricher = async ( } } } - + // We just use the values so we can store these as nested docs result.checks = checks.values(); result.put("@timestamp", maxTs); - - + + Map summary = new HashMap(); summary.up = checks.entrySet().stream().filter(c -> c.getValue().monitor.status == "up").count(); summary.down = checks.size() - summary.up; result.summary = summary; - + Map monitor = new HashMap(); monitor.ip = ips; monitor.name = name; - monitor.status = summary.down > 0 ? (summary.up > 0 ? "mixed": "down") : "up"; + monitor.status = summary.down > 0 ? "down" : "up"; result.monitor = monitor; - + Map observer = new HashMap(); Map geo = new HashMap(); observer.geo = geo; geo.name = geoNames; result.observer = observer; - + if (!podUids.isEmpty()) { result.kubernetes = new HashMap(); result.kubernetes.pod = new HashMap(); result.kubernetes.pod.uid = podUids; } - + if (!containerIds.isEmpty()) { result.container = new HashMap(); result.container.id = containerIds; } - + if (!tls.isEmpty()) { result.tls = new HashMap(); result.tls = tls; } - + return result; `, }, diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_chunk.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_chunk.ts index e395df0d1d08d..77676ac9a6373 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_chunk.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_chunk.ts @@ -6,8 +6,8 @@ import { refinePotentialMatches } from './refine_potential_matches'; import { findPotentialMatches } from './find_potential_matches'; -import { QueryContext } from '../elasticsearch_monitor_states_adapter'; import { ChunkFetcher, ChunkResult } from './monitor_group_iterator'; +import { QueryContext } from './query_context'; /** * Fetches a single 'chunk' of data with a single query, then uses a secondary query to filter out erroneous matches. diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts index 085c11f78b8f5..046bdc8a8d07d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts @@ -6,7 +6,7 @@ import { flatten } from 'lodash'; import { CursorPagination } from '../adapter_types'; -import { QueryContext } from '../elasticsearch_monitor_states_adapter'; +import { QueryContext } from './query_context'; import { QUERY } from '../../../../../common/constants'; import { CursorDirection, MonitorSummary, SortOrder } from '../../../../../common/graphql/types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; @@ -51,6 +51,7 @@ const fetchPageMonitorGroups: MonitorGroupsFetcher = async ( size: number ): Promise => { const monitorGroups: MonitorGroups[] = []; + const iterator = new MonitorGroupIterator(queryContext); let paginationBefore: CursorPagination | null = null; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts index 8f5e26b75f56c..e34bc6ab805c0 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts @@ -5,10 +5,9 @@ */ import { get, set } from 'lodash'; -import { QueryContext } from '../elasticsearch_monitor_states_adapter'; import { CursorDirection } from '../../../../../common/graphql/types'; import { INDEX_NAMES } from '../../../../../common/constants'; -import { makeDateRangeFilter } from '../../../helper/make_date_rate_filter'; +import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. // Note that these check groups may not be the most recent groups for the matching monitor ID! We'll filter those @@ -55,7 +54,7 @@ export const findPotentialMatches = async ( }; const query = async (queryContext: QueryContext, searchAfter: any, size: number) => { - const body = queryBody(queryContext, searchAfter, size); + const body = await queryBody(queryContext, searchAfter, size); const params = { index: INDEX_NAMES.HEARTBEAT, @@ -65,15 +64,11 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number) return await queryContext.search(params); }; -const queryBody = (queryContext: QueryContext, searchAfter: any, size: number) => { +const queryBody = async (queryContext: QueryContext, searchAfter: any, size: number) => { const compositeOrder = cursorDirectionToOrder(queryContext.pagination.cursorDirection); - const filters: any[] = [ - makeDateRangeFilter(queryContext.dateRangeStart, queryContext.dateRangeEnd), - ]; - if (queryContext.filterClause) { - filters.push(queryContext.filterClause); - } + const filters = await queryContext.dateAndCustomFilters(); + if (queryContext.statusFilter) { filters.push({ match: { 'monitor.status': queryContext.statusFilter } }); } @@ -82,6 +77,11 @@ const queryBody = (queryContext: QueryContext, searchAfter: any, size: number) = size: 0, query: { bool: { filter: filters } }, aggs: { + has_timespan: { + filter: { + exists: { field: 'monitor.timespan' }, + }, + }, monitors: { composite: { size, diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts index 1de2dbb0e364d..27c16863a37ab 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { QueryContext } from '../elasticsearch_monitor_states_adapter'; +import { QueryContext } from './query_context'; import { CursorPagination } from '../adapter_types'; import { fetchChunk } from './fetch_chunk'; import { CursorDirection } from '../../../../../common/graphql/types'; @@ -155,7 +155,7 @@ export class MonitorGroupIterator { // Returns a copy of this fetcher that goes backwards from the current position reverse(): MonitorGroupIterator | null { - const reverseContext = Object.assign({}, this.queryContext); + const reverseContext = this.queryContext.clone(); const current = this.getCurrent(); reverseContext.pagination = { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts new file mode 100644 index 0000000000000..03e228952f0e7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import DateMath from '@elastic/datemath'; +import { APICaller } from 'kibana/server'; +import { CursorPagination } from '../adapter_types'; +import { INDEX_NAMES } from '../../../../../common/constants'; + +export class QueryContext { + callES: APICaller; + dateRangeStart: string; + dateRangeEnd: string; + pagination: CursorPagination; + filterClause: any | null; + size: number; + statusFilter?: string; + hasTimespanCache?: boolean; + + constructor( + database: any, + dateRangeStart: string, + dateRangeEnd: string, + pagination: CursorPagination, + filterClause: any | null, + size: number, + statusFilter?: string + ) { + this.callES = database; + this.dateRangeStart = dateRangeStart; + this.dateRangeEnd = dateRangeEnd; + this.pagination = pagination; + this.filterClause = filterClause; + this.size = size; + this.statusFilter = statusFilter; + } + + async search(params: any): Promise { + params.index = INDEX_NAMES.HEARTBEAT; + return this.callES('search', params); + } + + async count(params: any): Promise { + params.index = INDEX_NAMES.HEARTBEAT; + return this.callES('count', params); + } + + async dateAndCustomFilters(): Promise { + const clauses = [await this.dateRangeFilter()]; + if (this.filterClause) { + clauses.push(this.filterClause); + } + return clauses; + } + + async dateRangeFilter(forceNoTimespan?: boolean): Promise { + const timestampClause = { + range: { '@timestamp': { gte: this.dateRangeStart, lte: this.dateRangeEnd } }, + }; + + if (forceNoTimespan === true || !(await this.hasTimespan())) { + return timestampClause; + } + + // @ts-ignore + const tsStart = DateMath.parse(this.dateRangeEnd).subtract(10, 'seconds'); + const tsEnd = DateMath.parse(this.dateRangeEnd)!; + + return { + bool: { + filter: [ + timestampClause, + { + bool: { + should: [ + { + range: { + 'monitor.timespan': { + gte: tsStart.toISOString(), + lte: tsEnd.toISOString(), + }, + }, + }, + { + bool: { + must_not: { exists: { field: 'monitor.timespan' } }, + }, + }, + ], + }, + }, + ], + }, + }; + } + + async hasTimespan(): Promise { + if (this.hasTimespanCache) { + return this.hasTimespanCache; + } + + this.hasTimespanCache = + ( + await this.count({ + body: { + query: { + bool: { + filter: [ + await this.dateRangeFilter(true), + { exists: { field: 'monitor.timespan' } }, + ], + }, + }, + }, + terminate_after: 1, + }) + ).count > 0; + + return this.hasTimespanCache; + } + + clone(): QueryContext { + return new QueryContext( + this.callES, + this.dateRangeStart, + this.dateRangeEnd, + this.pagination, + this.filterClause, + this.size, + this.statusFilter + ); + } +} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts index b0060cbee17bb..f8347d0737521 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts @@ -5,10 +5,9 @@ */ import { INDEX_NAMES } from '../../../../../common/constants'; -import { QueryContext } from '../elasticsearch_monitor_states_adapter'; +import { QueryContext } from './query_context'; import { CursorDirection } from '../../../../../common/graphql/types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; -import { makeDateRangeFilter } from '../../../helper/make_date_rate_filter'; /** * Determines whether the provided check groups are the latest complete check groups for their associated monitor ID's. @@ -103,7 +102,7 @@ export const mostRecentCheckGroups = async ( query: { bool: { filter: [ - makeDateRangeFilter(queryContext.dateRangeStart, queryContext.dateRangeEnd), + await queryContext.dateRangeFilter(), { terms: { 'monitor.id': potentialMatchMonitorIDs } }, // only match summary docs because we only want the latest *complete* check group. { exists: { field: 'summary' } }, diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap new file mode 100644 index 0000000000000..2f6d6e06f93e1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap @@ -0,0 +1,27 @@ +// 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/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap new file mode 100644 index 0000000000000..0f7abf5050bca --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateFilterAggs generates expected aggregations object 1`] = ` +Object { + "locations": Object { + "aggs": Object { + "term": Object { + "terms": Object { + "field": "observer.geo.name", + }, + }, + }, + "filter": Object { + "bool": Object { + "should": Array [ + Object { + "term": Object { + "url.port": "80", + }, + }, + Object { + "term": Object { + "url.port": "5601", + }, + }, + Object { + "term": Object { + "tags": "api", + }, + }, + Object { + "term": Object { + "monitor.type": "http", + }, + }, + Object { + "term": Object { + "monitor.type": "tcp", + }, + }, + ], + }, + }, + }, + "ports": Object { + "aggs": Object { + "term": Object { + "terms": Object { + "field": "url.port", + }, + }, + }, + "filter": Object { + "bool": Object { + "should": Array [ + Object { + "term": Object { + "observer.geo.name": "fairbanks", + }, + }, + Object { + "term": Object { + "observer.geo.name": "us-east-2", + }, + }, + Object { + "term": Object { + "tags": "api", + }, + }, + Object { + "term": Object { + "monitor.type": "http", + }, + }, + Object { + "term": Object { + "monitor.type": "tcp", + }, + }, + ], + }, + }, + }, + "schemes": Object { + "aggs": Object { + "term": Object { + "terms": Object { + "field": "monitor.type", + }, + }, + }, + "filter": Object { + "bool": Object { + "should": Array [ + Object { + "term": Object { + "observer.geo.name": "fairbanks", + }, + }, + Object { + "term": Object { + "observer.geo.name": "us-east-2", + }, + }, + Object { + "term": Object { + "url.port": "80", + }, + }, + Object { + "term": Object { + "url.port": "5601", + }, + }, + Object { + "term": Object { + "tags": "api", + }, + }, + ], + }, + }, + }, + "tags": Object { + "aggs": Object { + "term": Object { + "terms": Object { + "field": "tags", + }, + }, + }, + "filter": Object { + "bool": Object { + "should": Array [ + Object { + "term": Object { + "observer.geo.name": "fairbanks", + }, + }, + Object { + "term": Object { + "observer.geo.name": "us-east-2", + }, + }, + Object { + "term": Object { + "url.port": "80", + }, + }, + Object { + "term": Object { + "url.port": "5601", + }, + }, + Object { + "term": Object { + "monitor.type": "http", + }, + }, + Object { + "term": Object { + "monitor.type": "tcp", + }, + }, + ], + }, + }, + }, +} +`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/combine_range_with_filters.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/combine_range_with_filters.test.ts new file mode 100644 index 0000000000000..2075b3a8fbe0f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/combine_range_with_filters.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineRangeWithFilters } from '../elasticsearch_monitors_adapter'; + +describe('combineRangeWithFilters', () => { + it('combines filters that have no filter clause', () => { + expect( + combineRangeWithFilters('now-15m', 'now', { + bool: { should: [{ match: { 'url.port': 80 } }], minimum_should_match: 1 }, + }) + ).toEqual({ + bool: { + should: [ + { + match: { + 'url.port': 80, + }, + }, + ], + minimum_should_match: 1, + filter: [ + { + range: { + '@timestamp': { + gte: 'now-15m', + lte: 'now', + }, + }, + }, + ], + }, + }); + }); + + it('combines query with filter object', () => { + expect( + combineRangeWithFilters('now-15m', 'now', { + bool: { + filter: { term: { field: 'monitor.id' } }, + should: [{ match: { 'url.port': 80 } }], + minimum_should_match: 1, + }, + }) + ).toEqual({ + bool: { + filter: [ + { + field: 'monitor.id', + }, + { + range: { + '@timestamp': { + gte: 'now-15m', + lte: 'now', + }, + }, + }, + ], + should: [ + { + match: { + 'url.port': 80, + }, + }, + ], + minimum_should_match: 1, + }, + }); + }); + + it('combines query with filter list', () => { + expect( + combineRangeWithFilters('now-15m', 'now', { + bool: { + filter: [{ field: 'monitor.id' }], + should: [{ match: { 'url.port': 80 } }], + minimum_should_match: 1, + }, + }) + ).toEqual({ + bool: { + filter: [ + { + field: 'monitor.id', + }, + { + range: { + '@timestamp': { + gte: 'now-15m', + lte: 'now', + }, + }, + }, + ], + should: [ + { + match: { + 'url.port': 80, + }, + }, + ], + minimum_should_match: 1, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/extract_filter_aggs_results.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/extract_filter_aggs_results.test.ts new file mode 100644 index 0000000000000..954cffd4c9522 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/extract_filter_aggs_results.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { extractFilterAggsResults } from '../elasticsearch_monitors_adapter'; + +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/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/generate_filter_aggs.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/generate_filter_aggs.test.ts new file mode 100644 index 0000000000000..4e285ec25a492 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/generate_filter_aggs.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { generateFilterAggs } from '../generate_filter_aggs'; + +describe('generateFilterAggs', () => { + it('generates expected aggregations object', () => { + expect( + generateFilterAggs( + [ + { aggName: 'locations', filterName: 'locations', field: 'observer.geo.name' }, + { aggName: 'ports', filterName: 'ports', field: 'url.port' }, + { aggName: 'schemes', filterName: 'schemes', field: 'monitor.type' }, + { aggName: 'tags', filterName: 'tags', field: 'tags' }, + ], + { + locations: ['fairbanks', 'us-east-2'], + ports: ['80', '5601'], + tags: ['api'], + schemes: ['http', 'tcp'], + } + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts index b3d8cb855d55a..8523d9c75f51f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MonitorChart, MonitorPageTitle } from '../../../../common/graphql/types'; +import { MonitorChart } from '../../../../common/graphql/types'; import { UMElasticsearchQueryFn } from '../framework'; -import { MonitorDetails, MonitorLocations } from '../../../../common/runtime_types'; +import { + MonitorDetails, + MonitorLocations, + OverviewFilters, +} from '../../../../common/runtime_types'; export interface GetMonitorChartsDataParams { /** @member monitorId ID value for the selected monitor */ @@ -20,18 +24,21 @@ export interface GetMonitorChartsDataParams { } export interface GetFilterBarParams { + /** @param dateRangeStart timestamp bounds */ dateRangeStart: string; /** @member dateRangeEnd timestamp bounds */ dateRangeEnd: string; + /** @member search this value should correspond to Elasticsearch DSL + * generated from KQL text the user provided. + */ + search?: Record; + filterOptions: Record; } export interface GetMonitorDetailsParams { monitorId: string; -} - -export interface GetMonitorPageTitleParams { - /** @member monitorId the ID to query */ - monitorId: string; + dateStart: string; + dateEnd: string; } /** @@ -51,11 +58,13 @@ export interface UMMonitorsAdapter { * Fetches data used to populate monitor charts */ getMonitorChartsData: UMElasticsearchQueryFn; - getFilterBar: UMElasticsearchQueryFn; + /** - * Fetch data for the monitor page title. + * Fetch options for the filter bar. */ - getMonitorPageTitle: UMElasticsearchQueryFn<{ monitorId: string }, MonitorPageTitle | null>; + getFilterBar: UMElasticsearchQueryFn; + getMonitorDetails: UMElasticsearchQueryFn; + getMonitorLocations: UMElasticsearchQueryFn; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index b335205458965..37a9e032cd442 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -6,10 +6,56 @@ import { get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; -import { MonitorChart, Ping, LocationDurationLine } from '../../../../common/graphql/types'; +import { MonitorChart, LocationDurationLine } from '../../../../common/graphql/types'; import { getHistogramIntervalFormatted } from '../../helper'; import { MonitorError, MonitorLocation } from '../../../../common/runtime_types'; import { UMMonitorsAdapter } from './adapter_types'; +import { generateFilterAggs } from './generate_filter_aggs'; +import { OverviewFilters } from '../../../../common/runtime_types'; + +export const combineRangeWithFilters = ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: Record +) => { + const range = { + range: { + '@timestamp': { + gte: dateRangeStart, + lte: dateRangeEnd, + }, + }, + }; + if (!filters) return range; + const clientFiltersList = Array.isArray(filters?.bool?.filter ?? {}) + ? // i.e. {"bool":{"filter":{ ...some nested filter objects }}} + filters.bool.filter + : // i.e. {"bool":{"filter":[ ...some listed filter objects ]}} + Object.keys(filters?.bool?.filter ?? {}).map(key => ({ + ...filters?.bool?.filter?.[key], + })); + filters.bool.filter = [...clientFiltersList, range]; + return filters; +}; + +type SupportedFields = 'locations' | 'ports' | 'schemes' | 'tags'; + +export const extractFilterAggsResults = ( + responseAggregations: Record, + 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; +}; const formatStatusBuckets = (time: any, buckets: any, docCount: any) => { let up = null; @@ -160,78 +206,49 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = { return monitorChartsData; }, - getFilterBar: async ({ callES, dateRangeStart, dateRangeEnd }) => { - const fields: { [key: string]: string } = { - ids: 'monitor.id', - schemes: 'monitor.type', - urls: 'url.full', - ports: 'url.port', - locations: 'observer.geo.name', - }; + getFilterBar: async ({ callES, dateRangeStart, dateRangeEnd, search, filterOptions }) => { + const aggs = generateFilterAggs( + [ + { aggName: 'locations', filterName: 'locations', field: 'observer.geo.name' }, + { aggName: 'ports', filterName: 'ports', field: 'url.port' }, + { aggName: 'schemes', filterName: 'schemes', field: 'monitor.type' }, + { aggName: 'tags', filterName: 'tags', field: 'tags' }, + ], + filterOptions + ); + const filters = combineRangeWithFilters(dateRangeStart, dateRangeEnd, search); const params = { index: INDEX_NAMES.HEARTBEAT, body: { size: 0, query: { - range: { - '@timestamp': { - gte: dateRangeStart, - lte: dateRangeEnd, - }, - }, + ...filters, }, - aggs: Object.values(fields).reduce((acc: { [key: string]: any }, field) => { - acc[field] = { terms: { field, size: 20 } }; - return acc; - }, {}), + aggs, }, }; - const { aggregations } = await callES('search', params); - return Object.keys(fields).reduce((acc: { [key: string]: any[] }, field) => { - const bucketName = fields[field]; - acc[field] = aggregations[bucketName].buckets.map((b: { key: string | number }) => b.key); - return acc; - }, {}); + const { aggregations } = await callES('search', params); + return extractFilterAggsResults(aggregations, ['tags', 'locations', 'ports', 'schemes']); }, - getMonitorPageTitle: async ({ callES, monitorId }) => { - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - bool: { - filter: { - term: { - 'monitor.id': monitorId, - }, - }, + getMonitorDetails: async ({ callES, monitorId, dateStart, dateEnd }) => { + const queryFilters: any = [ + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, }, }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - size: 1, }, - }; - - const result = await callES('search', params); - const pageTitle: Ping | null = get(result, 'hits.hits[0]._source', null); - if (pageTitle === null) { - return null; - } - return { - id: get(pageTitle, 'monitor.id', null) || monitorId, - url: get(pageTitle, 'url.full', null), - name: get(pageTitle, 'monitor.name', null), - }; - }, + { + term: { + 'monitor.id': monitorId, + }, + }, + ]; - getMonitorDetails: async ({ callES, monitorId }) => { const params = { index: INDEX_NAMES.HEARTBEAT, body: { @@ -246,13 +263,7 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = { }, }, ], - filter: [ - { - term: { - 'monitor.id': monitorId, - }, - }, - ], + filter: queryFilters, }, }, sort: [ @@ -279,11 +290,6 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = { }; }, - /** - * Fetch data for the monitor page title. - * @param request Kibana server request - * - */ getMonitorLocations: async ({ callES, monitorId, dateStart, dateEnd }) => { const params = { index: INDEX_NAMES.HEARTBEAT, diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/generate_filter_aggs.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/generate_filter_aggs.ts new file mode 100644 index 0000000000000..26d412e33c868 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/generate_filter_aggs.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface AggDefinition { + aggName: string; + filterName: string; + field: string; +} + +export const FIELD_MAPPINGS: Record = { + schemes: 'monitor.type', + ports: 'url.port', + locations: 'observer.geo.name', + tags: 'tags', +}; + +const getFilterAggConditions = (filterTerms: Record, except: string) => { + const filters: any[] = []; + + Object.keys(filterTerms).forEach((key: string) => { + if (key === except && FIELD_MAPPINGS[key]) return; + filters.push( + ...filterTerms[key].map(value => ({ + term: { + [FIELD_MAPPINGS[key]]: value, + }, + })) + ); + }); + + return filters; +}; + +export const generateFilterAggs = ( + aggDefinitions: AggDefinition[], + filterOptions: Record +) => + aggDefinitions + .map(({ aggName, filterName, field }) => ({ + [aggName]: { + filter: { + bool: { + should: [...getFilterAggConditions(filterOptions, filterName)], + }, + }, + aggs: { + term: { + terms: { + field, + }, + }, + }, + }, + })) + .reduce((parent: Record, agg: any) => ({ ...parent, ...agg }), {}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts index bd1c182e938a3..e1e39ac9b2637 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts @@ -411,7 +411,7 @@ describe('ElasticsearchPingsAdapter class', () => { }); }); - describe('getLatestMonitorDocs', () => { + describe('getLatestMonitorStatus', () => { let expectedGetLatestSearchParams: any; beforeEach(() => { expectedGetLatestSearchParams = { @@ -429,7 +429,7 @@ describe('ElasticsearchPingsAdapter class', () => { }, }, { - term: { 'monitor.id': 'testmonitor' }, + term: { 'monitor.id': 'testMonitor' }, }, ], }, @@ -467,7 +467,7 @@ describe('ElasticsearchPingsAdapter class', () => { _source: { '@timestamp': 123456, monitor: { - id: 'testmonitor', + id: 'testMonitor', }, }, }, @@ -483,17 +483,16 @@ describe('ElasticsearchPingsAdapter class', () => { it('returns data in expected shape', async () => { const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); - const result = await adapter.getLatestMonitorDocs({ + const result = await adapter.getLatestMonitorStatus({ callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - monitorId: 'testmonitor', + dateStart: 'now-1h', + dateEnd: 'now', + monitorId: 'testMonitor', }); - expect(result).toHaveLength(1); - expect(result[0].timestamp).toBe(123456); - expect(result[0].monitor).not.toBeFalsy(); + expect(result.timestamp).toBe(123456); + expect(result.monitor).not.toBeFalsy(); // @ts-ignore monitor will be defined - expect(result[0].monitor.id).toBe('testmonitor'); + expect(result.monitor.id).toBe('testMonitor'); expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetLatestSearchParams); }); }); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts index 81df1c7c0f631..8b2a49c0c9ffe 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts @@ -33,16 +33,13 @@ export interface GetAllParams { export interface GetLatestMonitorDocsParams { /** @member dateRangeStart timestamp bounds */ - dateRangeStart: string; + dateStart?: string; /** @member dateRangeEnd timestamp bounds */ - dateRangeEnd: string; + dateEnd?: string; /** @member monitorId optional limit to monitorId */ monitorId?: string | null; - - /** @member location optional location value for use in filtering*/ - location?: string | null; } export interface GetPingHistogramParams { @@ -64,7 +61,10 @@ export interface GetPingHistogramParams { export interface UMPingsAdapter { getAll: UMElasticsearchQueryFn; - getLatestMonitorDocs: UMElasticsearchQueryFn; + // Get the monitor meta info regardless of timestamp + getMonitor: UMElasticsearchQueryFn; + + getLatestMonitorStatus: UMElasticsearchQueryFn; getPingHistogram: UMElasticsearchQueryFn; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts index 6862bed8d2bdd..adabffcb1ea4a 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts @@ -88,7 +88,10 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = { return results; }, - getLatestMonitorDocs: async ({ callES, dateRangeStart, dateRangeEnd, monitorId, location }) => { + // Get The monitor latest state sorted by timestamp with date range + getLatestMonitorStatus: async ({ callES, dateStart, dateEnd, monitorId }) => { + // TODO: Write tests for this function + const params = { index: INDEX_NAMES.HEARTBEAT, body: { @@ -98,13 +101,12 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = { { range: { '@timestamp': { - gte: dateRangeStart, - lte: dateRangeEnd, + gte: dateStart, + lte: dateEnd, }, }, }, ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), - ...(location ? [{ term: { 'observer.geo.name': location } }] : []), ], }, }, @@ -131,21 +133,45 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = { }; const result = await callES('search', params); - const buckets: any[] = get(result, 'aggregations.by_id.buckets', []); + const ping: any = result.aggregations.by_id.buckets?.[0]?.latest.hits?.hits?.[0] ?? {}; + + return { + ...ping?._source, + timestamp: ping?._source?.['@timestamp'], + }; + }, - return buckets.map( - ({ - latest: { - hits: { hits }, + // Get the monitor meta info regardless of timestamp + getMonitor: async ({ callES, monitorId }) => { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 1, + _source: ['url', 'monitor', 'observer'], + query: { + bool: { + filter: [ + { + term: { + 'monitor.id': monitorId, + }, + }, + ], + }, }, - }) => { - const timestamp = hits[0]._source[`@timestamp`]; - return { - ...hits[0]._source, - timestamp, - }; - } - ); + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + const result = await callES('search', params); + + return result.hits.hits[0]?._source; }, getPingHistogram: async ({ @@ -157,14 +183,14 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = { statusFilter, }) => { const boolFilters = parseFilterQuery(filters); - const additionaFilters = []; + const additionalFilters = []; if (monitorId) { - additionaFilters.push({ match: { 'monitor.id': monitorId } }); + additionalFilters.push({ match: { 'monitor.id': monitorId } }); } if (boolFilters) { - additionaFilters.push(boolFilters); + additionalFilters.push(boolFilters); } - const filter = getFilterClause(dateRangeStart, dateRangeEnd, additionaFilters); + const filter = getFilterClause(dateRangeStart, dateRangeEnd, additionalFilters); const interval = getHistogramInterval(dateRangeStart, dateRangeEnd); const intervalFormatted = getHistogramIntervalFormatted(dateRangeStart, dateRangeEnd); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts index 5259aa1d61711..c81fec933cb22 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -const getRange = (dateRangeStart: string, dateRangeEnd: string) => ({ - range: { - '@timestamp': { - gte: dateRangeStart, - lte: dateRangeEnd, - }, - }, -}); +import { makeDateRangeFilter } from './make_date_rate_filter'; export const getFilterClause = ( dateRangeStart: string, @@ -19,5 +12,5 @@ export const getFilterClause = ( additionalKeys?: Array<{ [key: string]: any }> ) => additionalKeys && additionalKeys.length > 0 - ? [getRange(dateRangeStart, dateRangeEnd), ...additionalKeys] - : [getRange(dateRangeStart, dateRangeEnd)]; + ? [makeDateRangeFilter(dateRangeStart, dateRangeEnd), ...additionalKeys] + : [makeDateRangeFilter(dateRangeStart, dateRangeEnd)]; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts index 4c88da7eca85a..f9a8de81332d5 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts @@ -9,3 +9,4 @@ export { getHistogramInterval } from './get_histogram_interval'; export { getHistogramIntervalFormatted } from './get_histogram_interval_formatted'; export { parseFilterQuery } from './parse_filter_query'; export { assertCloseTo } from './assert_close_to'; +export { objectValuesToArrays } from './object_to_array'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts new file mode 100644 index 0000000000000..334c31c822eaa --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Converts the top-level fields of an object from an object to an array. + * @param record the obect to map + * @type T the type of the objects/arrays that will be mapped + */ +export const objectValuesToArrays = (record: Record): Record => { + const obj: Record = {}; + Object.keys(record).forEach((key: string) => { + const value = record[key]; + obj[key] = value ? (Array.isArray(value) ? value : [value]) : []; + }); + return obj; +}; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index 4ab225076eff2..e64b317e67f98 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -4,21 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createGetOverviewFilters } from './overview_filters'; import { createGetAllRoute } from './pings'; import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteFactory } from './types'; -import { createGetMonitorDetailsRoute, createGetMonitorLocationsRoute } from './monitors'; +import { + createGetMonitorRoute, + createGetMonitorDetailsRoute, + createGetMonitorLocationsRoute, + createGetStatusBarRoute, +} from './monitors'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export { uptimeRouteWrapper } from './uptime_route_wrapper'; export const restApiRoutes: UMRestApiRouteFactory[] = [ + createGetOverviewFilters, createGetAllRoute, createGetIndexPatternRoute, + createGetMonitorRoute, createGetMonitorDetailsRoute, createGetMonitorLocationsRoute, + createGetStatusBarRoute, createGetSnapshotCount, createLogMonitorPageRoute, createLogOverviewPageRoute, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts index 2279233d49a09..7f1f10081dc4e 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts @@ -6,3 +6,4 @@ export { createGetMonitorDetailsRoute } from './monitors_details'; export { createGetMonitorLocationsRoute } from './monitor_locations'; +export { createGetMonitorRoute, createGetStatusBarRoute } from './status'; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts index a57e5ec469c59..9e1bc6f0d6a96 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -13,18 +13,24 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ path: '/api/uptime/monitor/details', validate: { query: schema.object({ - monitorId: schema.maybe(schema.string()), + monitorId: schema.string(), + dateStart: schema.maybe(schema.string()), + dateEnd: schema.maybe(schema.string()), }), }, options: { tags: ['access:uptime'], }, handler: async ({ callES }, _context, request, response): Promise => { - const { monitorId } = request.query; - + const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { - ...(await libs.monitors.getMonitorDetails({ callES, monitorId })), + ...(await libs.monitors.getMonitorDetails({ + callES, + monitorId, + dateStart, + dateEnd, + })), }, }); }, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts new file mode 100644 index 0000000000000..8b1bc04b45110 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createGetMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/monitor/selected', + validate: { + query: schema.object({ + monitorId: schema.string(), + }), + }, + options: { + tags: ['access:uptime'], + }, + handler: async ({ callES }, _context, request, response): Promise => { + const { monitorId } = request.query; + + return response.ok({ + body: { + ...(await libs.pings.getMonitor({ callES, monitorId })), + }, + }); + }, +}); + +export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/monitor/status', + validate: { + query: schema.object({ + monitorId: schema.string(), + dateStart: schema.string(), + dateEnd: schema.string(), + }), + }, + options: { + tags: ['access:uptime'], + }, + handler: async ({ callES }, _context, request, response): Promise => { + const { monitorId, dateStart, dateEnd } = request.query; + const result = await libs.pings.getLatestMonitorStatus({ + callES, + monitorId, + dateStart, + dateEnd, + }); + return response.ok({ + body: { + ...result, + }, + }); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts new file mode 100644 index 0000000000000..ef93253bb5b70 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; +import { objectValuesToArrays } from '../../lib/helper'; + +const arrayOrStringType = schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) +); + +export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/filters', + validate: { + query: schema.object({ + dateRangeStart: schema.string(), + dateRangeEnd: schema.string(), + search: schema.maybe(schema.string()), + locations: arrayOrStringType, + schemes: arrayOrStringType, + ports: arrayOrStringType, + tags: arrayOrStringType, + }), + }, + + options: { + tags: ['access:uptime'], + }, + handler: async ({ callES }, _context, request, response) => { + const { dateRangeStart, dateRangeEnd, locations, schemes, search, ports, tags } = request.query; + + let parsedSearch: Record | undefined; + if (search) { + try { + parsedSearch = JSON.parse(search); + } catch (e) { + return response.badRequest({ body: { message: e.message } }); + } + } + + const filtersResponse = await libs.monitors.getFilterBar({ + callES, + dateRangeStart, + dateRangeEnd, + search: parsedSearch, + filterOptions: objectValuesToArrays({ + locations, + ports, + schemes, + tags, + }), + }); + + return response.ok({ body: { ...filtersResponse } }); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts new file mode 100644 index 0000000000000..dc4e0c66a8183 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createGetOverviewFilters } from './get_overview_filters'; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts index 8cebe8ce26229..65648ae5f5a95 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts @@ -104,4 +104,5 @@ export type TestSubjects = | 'webhookPathInput' | 'webhookPortInput' | 'webhookMethodSelect' + | 'webhookSchemeSelect' | 'webhookUsernameInput'; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 36a5c150eead7..2800b0107da24 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -257,9 +257,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -324,9 +326,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -387,9 +391,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -461,9 +467,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -487,6 +495,7 @@ describe(' create route', () => { const METHOD = 'put'; const HOST = 'localhost'; const PORT = '9200'; + const SCHEME = 'http'; const PATH = '/test'; const USERNAME = 'test_user'; const PASSWORD = 'test_password'; @@ -510,6 +519,7 @@ describe(' create route', () => { form.setInputValue('webhookMethodSelect', METHOD); form.setInputValue('webhookHostInput', HOST); form.setInputValue('webhookPortInput', PORT); + form.setInputValue('webhookSchemeSelect', SCHEME); form.setInputValue('webhookPathInput', PATH); form.setInputValue('webhookUsernameInput', USERNAME); form.setInputValue('webhookPasswordInput', PASSWORD); @@ -534,6 +544,7 @@ describe(' create route', () => { method: METHOD, host: HOST, port: Number(PORT), + scheme: SCHEME, path: PATH, body: '{\n "message": "Watch [{{ctx.metadata.name}}] has exceeded the threshold"\n}', // Default @@ -551,9 +562,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -639,9 +652,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -707,9 +722,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; @@ -759,9 +776,11 @@ describe(' create route', () => { triggerIntervalUnit: 'm', aggType: 'count', termSize: 5, + termOrder: 'desc', thresholdComparator: '>', timeWindowSize: 5, timeWindowUnit: 'm', + hasTermsAgg: false, threshold: 1000, }; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index 1eee3d3b7e6ee..131400a8702c4 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -168,6 +168,7 @@ describe('', () => { }); const latestRequest = server.requests[server.requests.length - 1]; + const { id, type, @@ -194,9 +195,11 @@ describe('', () => { triggerIntervalUnit, aggType, termSize, + termOrder: 'desc', thresholdComparator, timeWindowSize, timeWindowUnit, + hasTermsAgg: false, threshold: threshold && threshold[0], }) ); diff --git a/x-pack/legacy/plugins/watcher/common/models/action/webhook_action.js b/x-pack/legacy/plugins/watcher/common/models/action/webhook_action.js index 54be8407f207d..d6f921a75a9ea 100644 --- a/x-pack/legacy/plugins/watcher/common/models/action/webhook_action.js +++ b/x-pack/legacy/plugins/watcher/common/models/action/webhook_action.js @@ -16,6 +16,7 @@ export class WebhookAction extends BaseAction { this.method = props.method; this.host = props.host; this.port = props.port; + this.scheme = props.scheme; this.path = props.path; this.body = props.body; this.contentType = props.contentType; @@ -30,6 +31,7 @@ export class WebhookAction extends BaseAction { method: this.method, host: this.host, port: this.port, + scheme: this.scheme, path: this.path, body: this.body, contentType: this.contentType, @@ -47,6 +49,7 @@ export class WebhookAction extends BaseAction { method: json.method, host: json.host, port: json.port, + scheme: json.scheme, path: json.path, body: json.body, contentType: json.contentType, @@ -72,6 +75,10 @@ export class WebhookAction extends BaseAction { optionalFields.method = this.method; } + if (this.scheme) { + optionalFields.scheme = this.scheme; + } + if (this.body) { optionalFields.body = this.body; } @@ -108,7 +115,7 @@ export class WebhookAction extends BaseAction { const webhookJson = json && json.actionJson && json.actionJson.webhook; const { errors } = this.validateJson(json.actionJson); - const { path, method, body, auth, headers } = webhookJson; + const { path, method, scheme, body, auth, headers } = webhookJson; const optionalFields = {}; @@ -120,6 +127,10 @@ export class WebhookAction extends BaseAction { optionalFields.method = method; } + if (scheme) { + optionalFields.scheme = scheme; + } + if (body) { optionalFields.body = body; } diff --git a/x-pack/legacy/plugins/watcher/common/types/action_types.ts b/x-pack/legacy/plugins/watcher/common/types/action_types.ts index 123bf0f58db9d..918e9a933611b 100644 --- a/x-pack/legacy/plugins/watcher/common/types/action_types.ts +++ b/x-pack/legacy/plugins/watcher/common/types/action_types.ts @@ -56,6 +56,7 @@ export interface WebhookAction extends BaseAction { method?: 'head' | 'get' | 'post' | 'put' | 'delete'; host: string; port: number; + scheme?: 'http' | 'https'; path?: string; body?: string; username?: string; diff --git a/x-pack/legacy/plugins/watcher/public/legacy.ts b/x-pack/legacy/plugins/watcher/public/legacy.ts index d7b85ccfeb7b4..21fcd718ea1b7 100644 --- a/x-pack/legacy/plugins/watcher/public/legacy.ts +++ b/x-pack/legacy/plugins/watcher/public/legacy.ts @@ -43,6 +43,7 @@ routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param app.mount(npStart as any, { element: elem, appBasePath: '/management/elasticsearch/watcher/', + onAppLeave: () => undefined, }); }, }, diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js index d46d9aacb035b..6f496dd9ee138 100644 --- a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js @@ -11,23 +11,21 @@ import { i18n } from '@kbn/i18n'; export class WebhookAction extends BaseAction { constructor(props = {}) { super(props); - const defaultJson = JSON.stringify( { message: 'Watch [{{ctx.metadata.name}}] has exceeded the threshold' }, null, 2 ); this.body = get(props, 'body', props.ignoreDefaults ? null : defaultJson); - this.method = get(props, 'method'); this.host = get(props, 'host'); this.port = get(props, 'port'); + this.scheme = get(props, 'scheme', 'http'); this.path = get(props, 'path'); this.username = get(props, 'username'); this.password = get(props, 'password'); this.contentType = get(props, 'contentType'); - - this.fullPath = `${this.host}:${this.port}${this.path}`; + this.fullPath = `${this.host}:${this.port}${this.path ? '/' + this.path : ''}`; } validate() { @@ -112,6 +110,7 @@ export class WebhookAction extends BaseAction { method: this.method, host: this.host, port: this.port, + scheme: this.scheme, path: this.path, body: this.body, username: this.username, diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js index 7611f158fb962..2383388dd89bf 100644 --- a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js @@ -256,9 +256,11 @@ export class ThresholdWatch extends BaseWatch { aggField: this.aggField, termSize: this.termSize, termField: this.termField, + termOrder: this.termOrder, thresholdComparator: this.thresholdComparator, timeWindowSize: this.timeWindowSize, timeWindowUnit: this.timeWindowUnit, + hasTermsAgg: this.hasTermsAgg, threshold: comparators[this.thresholdComparator].requiredValues > 1 ? this.threshold diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx index bdc6f0bcbb717..be0b551f4a39c 100644 --- a/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx @@ -29,13 +29,15 @@ interface Props { const HTTP_VERBS = ['head', 'get', 'post', 'put', 'delete']; +const SCHEME = ['http', 'https']; + export const WebhookActionFields: React.FunctionComponent = ({ action, editAction, errors, hasErrors, }) => { - const { method, host, port, path, body, username, password } = action; + const { method, host, port, scheme, path, body, username, password } = action; useEffect(() => { editAction({ key: 'contentType', value: 'application/json' }); // set content-type for threshold watch to json by default @@ -65,6 +67,27 @@ export const WebhookActionFields: React.FunctionComponent = ({
+ + + ({ text: verb, value: verb }))} + onChange={e => { + editAction({ key: 'scheme', value: e.target.value }); + }} + /> + + + { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js index bf7889473b60d..c83fbc0b4564c 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js @@ -188,50 +188,6 @@ describe('BaseWatch', () => { }); }); - describe('upstreamJson getter method', () => { - let props; - beforeEach(() => { - props = { - id: 'foo', - name: 'bar', - type: 'json', - watchStatus: { - downstreamJson: { - prop1: 'prop1', - prop2: 'prop2', - }, - }, - actions: [ - { - downstreamJson: { - prop1: 'prop3', - prop2: 'prop4', - }, - }, - ], - }; - }); - - it('should return a valid object', () => { - const watch = new BaseWatch(props); - - const actual = watch.upstreamJson; - const expected = { - id: props.id, - watch: { - metadata: { - name: props.name, - xpack: { - type: props.type, - }, - }, - }, - }; - - expect(actual).toEqual(expected); - }); - }); - describe('getPropsFromDownstreamJson method', () => { let downstreamJson; beforeEach(() => { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js index e6d49f7adc19e..2440d4ef33881 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js @@ -23,12 +23,6 @@ export class JsonWatch extends BaseWatch { return serializeJsonWatch(this.name, this.watch); } - // To Elasticsearch - get upstreamJson() { - const result = super.upstreamJson; - return result; - } - // To Kibana get downstreamJson() { const result = merge({}, super.downstreamJson, { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js index 56150667b609e..0301c4c95be94 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js @@ -52,26 +52,6 @@ describe('JsonWatch', () => { }); }); - describe('upstreamJson getter method', () => { - it('should return the correct result', () => { - const watch = new JsonWatch({ watch: { foo: 'bar' } }); - const actual = watch.upstreamJson; - const expected = { - id: undefined, - watch: { - foo: 'bar', - metadata: { - xpack: { - type: 'json', - }, - }, - }, - }; - - expect(actual).toEqual(expected); - }); - }); - describe('downstreamJson getter method', () => { let props; beforeEach(() => { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js index 21909c488431f..e5410588ab566 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js @@ -55,12 +55,6 @@ export class ThresholdWatch extends BaseWatch { return formatVisualizeData(this, results); } - // To Elasticsearch - get upstreamJson() { - const result = super.upstreamJson; - return result; - } - // To Kibana get downstreamJson() { const result = merge({}, super.downstreamJson, { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts b/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts index 2e8c81efa19c0..a311c31082183 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts @@ -3,7 +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. */ -import { first } from 'rxjs/operators'; import { Plugin, CoreSetup } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { PLUGIN } from '../../common/constants'; @@ -23,7 +22,7 @@ export class WatcherServerPlugin implements Plugin { { http, elasticsearch: elasticsearchService }: CoreSetup, { __LEGACY: serverShim }: { __LEGACY: ServerShim } ) { - const elasticsearch = await elasticsearchService.adminClient$.pipe(first()).toPromise(); + const elasticsearch = await elasticsearchService.adminClient; const router = http.createRouter(); const routeDependencies: RouteDependencies = { elasticsearch, diff --git a/x-pack/package.json b/x-pack/package.json index 110db56c5d4ed..1e20157831ba5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -96,7 +96,7 @@ "@types/react-test-renderer": "^16.9.1", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^0.3.0", - "@types/redux-actions": "^2.2.1", + "@types/redux-actions": "^2.6.1", "@types/sinon": "^7.0.13", "@types/styled-components": "^4.4.1", "@types/supertest": "^2.0.5", @@ -192,6 +192,7 @@ "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", "@turf/boolean-contains": "6.0.1", + "angular": "^1.7.9", "angular-resource": "1.7.8", "angular-sanitize": "1.7.8", "angular-ui-ace": "0.2.3", @@ -313,7 +314,7 @@ "recompose": "^0.26.0", "reduce-reducers": "^0.4.3", "redux": "4.0.0", - "redux-actions": "2.2.1", + "redux-actions": "2.6.5", "redux-observable": "^1.0.0", "redux-saga": "^0.16.0", "redux-thunk": "2.3.0", @@ -321,6 +322,7 @@ "request": "^2.88.0", "reselect": "3.0.1", "resize-observer-polyfill": "^1.5.0", + "re-resizable": "^6.1.1", "rison-node": "0.3.1", "rxjs": "^6.5.3", "semver": "5.7.0", @@ -341,7 +343,7 @@ "uuid": "3.3.2", "venn.js": "0.2.20", "vscode-languageserver": "^5.2.1", - "webpack": "4.33.0", + "webpack": "4.41.0", "wellknown": "^0.5.0", "xml2js": "^0.4.22", "xregexp": "4.2.4" diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index a1cf2ae4e8ead..11d91efdf6b01 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -58,16 +58,14 @@ export class APMPlugin implements Plugin { }); await new Promise(resolve => { - combineLatest(mergedConfig$, core.elasticsearch.dataClient$).subscribe( - async ([config, dataClient]) => { - this.currentConfig = config; - await createApmAgentConfigurationIndex({ - esClient: dataClient, - config, - }); - resolve(); - } - ); + mergedConfig$.subscribe(async config => { + this.currentConfig = config; + await createApmAgentConfigurationIndex({ + esClient: core.elasticsearch.dataClient, + config, + }); + resolve(); + }); }); return { diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md new file mode 100644 index 0000000000000..c0acb87835207 --- /dev/null +++ b/x-pack/plugins/case/README.md @@ -0,0 +1,9 @@ +# Case Workflow + +*Experimental Feature* + +Elastic is developing a Case Management Workflow. Follow our progress: + +- [Case API Documentation](https://documenter.getpostman.com/view/172706/SW7c2SuF?version=latest) +- [Github Meta](https://github.com/elastic/kibana/issues/50103) + diff --git a/x-pack/plugins/case/kibana.json b/x-pack/plugins/case/kibana.json new file mode 100644 index 0000000000000..23e3cc789ad3b --- /dev/null +++ b/x-pack/plugins/case/kibana.json @@ -0,0 +1,9 @@ +{ + "configPath": ["xpack", "case"], + "id": "case", + "kibanaVersion": "kibana", + "requiredPlugins": ["security"], + "server": true, + "ui": false, + "version": "8.0.0" +} diff --git a/x-pack/plugins/case/server/config.ts b/x-pack/plugins/case/server/config.ts new file mode 100644 index 0000000000000..a7cb117198f9b --- /dev/null +++ b/x-pack/plugins/case/server/config.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + indexPattern: schema.string({ defaultValue: '.case-test-2' }), + secret: schema.string({ defaultValue: 'Cool secret huh?' }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/case/server/constants.ts b/x-pack/plugins/case/server/constants.ts new file mode 100644 index 0000000000000..276dcd135254a --- /dev/null +++ b/x-pack/plugins/case/server/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const CASE_SAVED_OBJECT = 'case-workflow'; +export const CASE_COMMENT_SAVED_OBJECT = 'case-workflow-comment'; diff --git a/x-pack/plugins/case/server/index.ts b/x-pack/plugins/case/server/index.ts new file mode 100644 index 0000000000000..3963debea9795 --- /dev/null +++ b/x-pack/plugins/case/server/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 { PluginInitializerContext } from '../../../../src/core/server'; +import { ConfigSchema } from './config'; +import { CasePlugin } from './plugin'; +export { NewCaseFormatted, NewCommentFormatted } from './routes/api/types'; + +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext) => + new CasePlugin(initializerContext); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts new file mode 100644 index 0000000000000..c52461cade058 --- /dev/null +++ b/x-pack/plugins/case/server/plugin.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first, map } from 'rxjs/operators'; +import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; +import { ConfigType } from './config'; +import { initCaseApi } from './routes/api'; +import { CaseService } from './services'; +import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; + +function createConfig$(context: PluginInitializerContext) { + return context.config.create().pipe(map(config => config)); +} + +export interface PluginsSetup { + security: SecurityPluginSetup; +} + +export class CasePlugin { + private readonly log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: PluginsSetup) { + const config = await createConfig$(this.initializerContext) + .pipe(first()) + .toPromise(); + + if (!config.enabled) { + return; + } + const service = new CaseService(this.log); + + this.log.debug( + `Setting up Case Workflow with core contract [${Object.keys( + core + )}] and plugins [${Object.keys(plugins)}]` + ); + + const caseService = await service.setup({ + authentication: plugins.security.authc, + }); + + const router = core.http.createRouter(); + initCaseApi({ + caseService, + router, + }); + } + + public start() { + this.log.debug(`Starting Case Workflow`); + } + + public stop() { + this.log.debug(`Stopping Case Workflow`); + } +} diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts new file mode 100644 index 0000000000000..94ce9627b9ac6 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Authentication } from '../../../../../security/server'; + +const getCurrentUser = jest.fn().mockReturnValue({ + username: 'awesome', + full_name: 'Awesome D00d', +}); +const getCurrentUserThrow = jest.fn().mockImplementation(() => { + throw new Error('Bad User - the user is not authenticated'); +}); + +export const authenticationMock = { + create: (): jest.Mocked => ({ + login: jest.fn(), + createAPIKey: jest.fn(), + getCurrentUser, + invalidateAPIKey: jest.fn(), + isAuthenticated: jest.fn(), + logout: jest.fn(), + getSessionInfo: jest.fn(), + }), + createInvalid: (): jest.Mocked => ({ + login: jest.fn(), + createAPIKey: jest.fn(), + getCurrentUser: getCurrentUserThrow, + invalidateAPIKey: jest.fn(), + isAuthenticated: jest.fn(), + logout: jest.fn(), + getSessionInfo: jest.fn(), + }), +}; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts new file mode 100644 index 0000000000000..360c6de67b2a8 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../../constants'; + +export const createMockSavedObjectsRepository = (savedObject: any[] = []) => { + const mockSavedObjectsClientContract = ({ + get: jest.fn((type, id) => { + const result = savedObject.filter(s => s.id === id); + if (!result.length) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return result[0]; + }), + find: jest.fn(findArgs => { + if (findArgs.hasReference && findArgs.hasReference.id === 'bad-guy') { + throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); + } + return { + total: savedObject.length, + saved_objects: savedObject, + }; + }), + create: jest.fn((type, attributes, references) => { + if (attributes.description === 'Throw an error' || attributes.comment === 'Throw an error') { + throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); + } + if (type === CASE_COMMENT_SAVED_OBJECT) { + return { + type, + id: 'mock-comment', + attributes, + ...references, + updated_at: '2019-12-02T22:48:08.327Z', + version: 'WzksMV0=', + }; + } + return { + type, + id: 'mock-it', + attributes, + references: [], + updated_at: '2019-12-02T22:48:08.327Z', + version: 'WzksMV0=', + }; + }), + update: jest.fn((type, id, attributes) => { + if (!savedObject.find(s => s.id === id)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return { + id, + type, + updated_at: '2019-11-22T22:50:55.191Z', + version: 'WzE3LDFd', + attributes, + }; + }), + delete: jest.fn((type: string, id: string) => { + const result = savedObject.filter(s => s.id === id); + if (!result.length) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + if (type === 'case-workflow-comment' && id === 'bad-guy') { + throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); + } + return {}; + }), + deleteByNamespace: jest.fn(), + } as unknown) as jest.Mocked; + + return mockSavedObjectsClientContract; +}; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts new file mode 100644 index 0000000000000..e1fec2d6b229c --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { mockCases, mockCasesErrorTriggerData, mockCaseComments } from './mock_saved_objects'; +export { createMockSavedObjectsRepository } from './create_mock_so_repository'; +export { createRouteContext } from './route_contexts'; +export { authenticationMock } from './authc_mock'; +export { createRoute } from './mock_router'; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts new file mode 100644 index 0000000000000..84889c3ac49be --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; +import { loggingServiceMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; +import { CaseService } from '../../../services'; +import { authenticationMock } from '../__fixtures__'; +import { RouteDeps } from '../index'; + +export const createRoute = async ( + api: (deps: RouteDeps) => void, + method: 'get' | 'post' | 'delete', + badAuth = false +) => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + + const log = loggingServiceMock.create().get('case'); + + const service = new CaseService(log); + const caseService = await service.setup({ + authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), + }); + + api({ + router, + caseService, + }); + + return router[method].mock.calls[0][1]; +}; 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 new file mode 100644 index 0000000000000..d59f0977e6993 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 mockCases = [ + { + type: 'case-workflow', + id: 'mock-id-1', + attributes: { + created_at: 1574718888885, + created_by: { + full_name: null, + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + state: 'open', + tags: ['defacement'], + case_type: 'security', + assignees: [], + }, + references: [], + updated_at: '2019-11-25T21:54:48.952Z', + version: 'WzAsMV0=', + }, + { + type: 'case-workflow', + id: 'mock-id-2', + attributes: { + created_at: 1574721120834, + created_by: { + full_name: null, + username: 'elastic', + }, + description: 'Oh no, a bad meanie destroying data!', + title: 'Damaging Data Destruction Detected', + state: 'open', + tags: ['Data Destruction'], + case_type: 'security', + assignees: [], + }, + references: [], + updated_at: '2019-11-25T22:32:00.900Z', + version: 'WzQsMV0=', + }, + { + type: 'case-workflow', + id: 'mock-id-3', + attributes: { + created_at: 1574721137881, + created_by: { + full_name: null, + username: 'elastic', + }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + title: 'Another bad one', + state: 'open', + tags: ['LOLBins'], + case_type: 'security', + assignees: [], + }, + references: [], + updated_at: '2019-11-25T22:32:17.947Z', + version: 'WzUsMV0=', + }, +]; + +export const mockCasesErrorTriggerData = [ + { + id: 'valid-id', + }, + { + id: 'bad-guy', + }, +]; + +export const mockCaseComments = [ + { + type: 'case-workflow-comment', + id: 'mock-comment-1', + attributes: { + comment: 'Wow, good luck catching that bad meanie!', + created_at: 1574718900112, + created_by: { + full_name: null, + username: 'elastic', + }, + }, + references: [ + { + type: 'case-workflow', + name: 'associated-case-workflow', + id: 'mock-id-1', + }, + ], + updated_at: '2019-11-25T21:55:00.177Z', + version: 'WzEsMV0=', + }, + { + type: 'case-workflow-comment', + id: 'mock-comment-2', + attributes: { + comment: 'Well I decided to update my comment. So what? Deal with it.', + created_at: 1574718902724, + created_by: { + full_name: null, + username: 'elastic', + }, + }, + references: [ + { + type: 'case-workflow', + name: 'associated-case-workflow', + id: 'mock-id-1', + }, + ], + updated_at: '2019-11-25T21:55:14.633Z', + version: 'WzMsMV0=', + }, + { + type: 'case-workflow-comment', + id: 'mock-comment-3', + attributes: { + comment: 'Wow, good luck catching that bad meanie!', + created_at: 1574721150542, + created_by: { + full_name: null, + username: 'elastic', + }, + }, + references: [ + { + type: 'case-workflow', + name: 'associated-case-workflow', + id: 'mock-id-3', + }, + ], + updated_at: '2019-11-25T22:32:30.608Z', + version: 'WzYsMV0=', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/is_resizing.tsx b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts similarity index 51% rename from x-pack/legacy/plugins/siem/public/components/resize_handle/is_resizing.tsx rename to x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 5eb2d397b4c98..b1881e394e796 100644 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/is_resizing.tsx +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState } from 'react'; +import { RequestHandlerContext } from 'src/core/server'; -export const useIsContainerResizing = () => { - const [isResizing, setIsResizing] = useState(false); - - return { - isResizing, - setIsResizing, - }; +export const createRouteContext = (client: any) => { + return ({ + core: { + savedObjects: { + client, + }, + }, + } as unknown) as RequestHandlerContext; }; diff --git a/x-pack/plugins/case/server/routes/api/__tests__/delete_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/delete_case.test.ts new file mode 100644 index 0000000000000..9ea42ba42406b --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/delete_case.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, + mockCasesErrorTriggerData, +} from '../__fixtures__'; +import { initDeleteCaseApi } from '../delete_case'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('DELETE case', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initDeleteCaseApi, 'delete'); + }); + it(`deletes the case. responds with 204`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'delete', + params: { + id: 'mock-id-1', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(204); + }); + it(`returns an error when thrown from deleteCase service`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'delete', + params: { + id: 'not-real', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + }); + it(`returns an error when thrown from getAllCaseComments service`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'delete', + params: { + id: 'bad-guy', + }, + }); + + const theContext = createRouteContext( + createMockSavedObjectsRepository(mockCasesErrorTriggerData) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + }); + it(`returns an error when thrown from deleteComment service`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'delete', + params: { + id: 'valid-id', + }, + }); + + const theContext = createRouteContext( + createMockSavedObjectsRepository(mockCasesErrorTriggerData) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/delete_comment.test.ts new file mode 100644 index 0000000000000..e50b3cbaa9c9a --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/delete_comment.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, + mockCasesErrorTriggerData, +} from '../__fixtures__'; +import { initDeleteCommentApi } from '../delete_comment'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('DELETE comment', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initDeleteCommentApi, 'delete'); + }); + it(`deletes the comment. responds with 204`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comments/{comment_id}', + method: 'delete', + params: { + comment_id: 'mock-id-1', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(204); + }); + it(`returns an error when thrown from deleteComment service`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comments/{comment_id}', + method: 'delete', + params: { + comment_id: 'bad-guy', + }, + }); + + const theContext = createRouteContext( + createMockSavedObjectsRepository(mockCasesErrorTriggerData) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts new file mode 100644 index 0000000000000..2f8a229c08f29 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, +} from '../__fixtures__'; +import { initGetAllCasesApi } from '../get_all_cases'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('GET all cases', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initGetAllCasesApi, 'get'); + }); + it(`returns the case without case comments when includeComments is false`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'get', + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.saved_objects).toHaveLength(3); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts new file mode 100644 index 0000000000000..3c5f8e52d1946 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, + mockCasesErrorTriggerData, +} from '../__fixtures__'; +import { initGetCaseApi } from '../get_case'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('GET case', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initGetCaseApi, 'get'); + }); + it(`returns the case without case comments when includeComments is false`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + params: { + id: 'mock-id-1', + }, + method: 'get', + query: { + includeComments: false, + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + + expect(response.status).toEqual(200); + expect(response.payload).toEqual(mockCases.find(s => s.id === 'mock-id-1')); + expect(response.payload.comments).toBeUndefined(); + }); + it(`returns an error when thrown from getCase`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + params: { + id: 'abcdefg', + }, + method: 'get', + query: { + includeComments: false, + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + + expect(response.status).toEqual(404); + expect(response.payload.isBoom).toEqual(true); + }); + it(`returns the case with case comments when includeComments is true`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + params: { + id: 'mock-id-1', + }, + method: 'get', + query: { + includeComments: true, + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + + expect(response.status).toEqual(200); + expect(response.payload.comments.saved_objects).toHaveLength(3); + }); + it(`returns an error when thrown from getAllCaseComments`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + params: { + id: 'bad-guy', + }, + method: 'get', + query: { + includeComments: true, + }, + }); + + const theContext = createRouteContext( + createMockSavedObjectsRepository(mockCasesErrorTriggerData) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + + expect(response.status).toEqual(400); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts new file mode 100644 index 0000000000000..9b6a1e435838b --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCaseComments, +} from '../__fixtures__'; +import { initGetCommentApi } from '../get_comment'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('GET comment', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initGetCommentApi, 'get'); + }); + it(`returns the comment`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comments/{id}', + method: 'get', + params: { + id: 'mock-comment-1', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload).toEqual(mockCaseComments.find(s => s.id === 'mock-comment-1')); + }); + it(`returns an error when getComment throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comments/{id}', + method: 'get', + params: { + id: 'not-real', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts new file mode 100644 index 0000000000000..bb688dde4c58f --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts @@ -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 { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, +} from '../__fixtures__'; +import { initPostCaseApi } from '../post_case'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('POST cases', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initPostCaseApi, 'post'); + }); + it(`Posts a new case`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'post', + body: { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + state: 'open', + tags: ['defacement'], + case_type: 'security', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.id).toEqual('mock-it'); + expect(response.payload.attributes.created_by.username).toEqual('awesome'); + }); + it(`Returns an error if postNewCase throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'post', + body: { + description: 'Throw an error', + title: 'Super Bad Security Issue', + state: 'open', + tags: ['error'], + case_type: 'security', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + }); + it(`Returns an error if user authentication throws`, async () => { + routeHandler = await createRoute(initPostCaseApi, 'post', true); + + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'post', + body: { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + state: 'open', + tags: ['defacement'], + case_type: 'security', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(500); + expect(response.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts new file mode 100644 index 0000000000000..0c059b7f15ea4 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, +} from '../__fixtures__'; +import { initPostCommentApi } from '../post_comment'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('POST comment', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initPostCommentApi, 'post'); + }); + it(`Posts a new comment`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}/comment', + method: 'post', + params: { + id: 'mock-id-1', + }, + body: { + comment: 'Wow, good luck catching that bad meanie!', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.id).toEqual('mock-comment'); + expect(response.payload.references[0].id).toEqual('mock-id-1'); + }); + it(`Returns an error if the case does not exist`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}/comment', + method: 'post', + params: { + id: 'this-is-not-real', + }, + body: { + comment: 'Wow, good luck catching that bad meanie!', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + expect(response.payload.isBoom).toEqual(true); + }); + it(`Returns an error if postNewCase throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}/comment', + method: 'post', + params: { + id: 'mock-id-1', + }, + body: { + comment: 'Throw an error', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + }); + it(`Returns an error if user authentication throws`, async () => { + routeHandler = await createRoute(initPostCommentApi, 'post', true); + + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}/comment', + method: 'post', + params: { + id: 'mock-id-1', + }, + body: { + comment: 'Wow, good luck catching that bad meanie!', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(500); + expect(response.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts new file mode 100644 index 0000000000000..7ed478d2e7c01 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, +} from '../__fixtures__'; +import { initUpdateCaseApi } from '../update_case'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('UPDATE case', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initUpdateCaseApi, 'post'); + }); + it(`Updates a case`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'post', + params: { + id: 'mock-id-1', + }, + body: { + state: 'closed', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.id).toEqual('mock-id-1'); + expect(response.payload.attributes.state).toEqual('closed'); + }); + it(`Returns an error if updateCase throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'post', + params: { + id: 'mock-id-does-not-exist', + }, + body: { + state: 'closed', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + expect(response.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts new file mode 100644 index 0000000000000..8aa84b45b7dbb --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCaseComments, +} from '../__fixtures__'; +import { initUpdateCommentApi } from '../update_comment'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('UPDATE comment', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initUpdateCommentApi, 'post'); + }); + it(`Updates a comment`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comment/{id}', + method: 'post', + params: { + id: 'mock-comment-1', + }, + body: { + comment: 'Update my comment', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.id).toEqual('mock-comment-1'); + expect(response.payload.attributes.comment).toEqual('Update my comment'); + }); + it(`Returns an error if updateComment throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comment/{id}', + method: 'post', + params: { + id: 'mock-comment-does-not-exist', + }, + body: { + comment: 'Update my comment', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + expect(response.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/delete_case.ts b/x-pack/plugins/case/server/routes/api/delete_case.ts new file mode 100644 index 0000000000000..a5ae72b8b46ff --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/delete_case.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDeps } from '.'; +import { wrapError } from './utils'; + +export function initDeleteCaseApi({ caseService, router }: RouteDeps) { + router.delete( + { + path: '/api/cases/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + let allCaseComments; + try { + await caseService.deleteCase({ + client: context.core.savedObjects.client, + caseId: request.params.id, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + try { + allCaseComments = await caseService.getAllCaseComments({ + client: context.core.savedObjects.client, + caseId: request.params.id, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + try { + if (allCaseComments.saved_objects.length > 0) { + await Promise.all( + allCaseComments.saved_objects.map(({ id }) => + caseService.deleteComment({ + client: context.core.savedObjects.client, + commentId: id, + }) + ) + ); + } + return response.noContent(); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/delete_comment.ts b/x-pack/plugins/case/server/routes/api/delete_comment.ts new file mode 100644 index 0000000000000..4a540dd9fd69f --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/delete_comment.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDeps } from '.'; +import { wrapError } from './utils'; + +export function initDeleteCommentApi({ caseService, router }: RouteDeps) { + router.delete( + { + path: '/api/cases/comments/{comment_id}', + validate: { + params: schema.object({ + comment_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = context.core.savedObjects.client; + try { + await caseService.deleteComment({ + client, + commentId: request.params.comment_id, + }); + return response.noContent(); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts b/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts new file mode 100644 index 0000000000000..cc4956ead1bd7 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDeps } from '.'; +import { wrapError } from './utils'; + +export function initGetAllCaseCommentsApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases/{id}/comments', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + try { + const theComments = await caseService.getAllCaseComments({ + client: context.core.savedObjects.client, + caseId: request.params.id, + }); + return response.ok({ body: theComments }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts new file mode 100644 index 0000000000000..749a183dfe980 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDeps } from '.'; +import { wrapError } from './utils'; + +export function initGetAllCasesApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases', + validate: false, + }, + async (context, request, response) => { + try { + const cases = await caseService.getAllCases({ + client: context.core.savedObjects.client, + }); + return response.ok({ body: cases }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/get_case.ts b/x-pack/plugins/case/server/routes/api/get_case.ts new file mode 100644 index 0000000000000..6aad22a1ebf1b --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_case.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDeps } from '.'; +import { wrapError } from './utils'; + +export function initGetCaseApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + query: schema.object({ + includeComments: schema.string({ defaultValue: 'true' }), + }), + }, + }, + async (context, request, response) => { + let theCase; + const includeComments = JSON.parse(request.query.includeComments); + try { + theCase = await caseService.getCase({ + client: context.core.savedObjects.client, + caseId: request.params.id, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + if (!includeComments) { + return response.ok({ body: theCase }); + } + try { + const theComments = await caseService.getAllCaseComments({ + client: context.core.savedObjects.client, + caseId: request.params.id, + }); + return response.ok({ body: { ...theCase, comments: theComments } }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/get_comment.ts b/x-pack/plugins/case/server/routes/api/get_comment.ts new file mode 100644 index 0000000000000..6fd507d89738d --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_comment.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDeps } from '.'; +import { wrapError } from './utils'; + +export function initGetCommentApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases/comments/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + try { + const theComment = await caseService.getComment({ + client: context.core.savedObjects.client, + commentId: request.params.id, + }); + return response.ok({ body: theComment }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts new file mode 100644 index 0000000000000..11ef91d539e87 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; +import { initDeleteCommentApi } from './delete_comment'; +import { initDeleteCaseApi } from './delete_case'; +import { initGetAllCaseCommentsApi } from './get_all_case_comments'; +import { initGetAllCasesApi } from './get_all_cases'; +import { initGetCaseApi } from './get_case'; +import { initGetCommentApi } from './get_comment'; +import { initPostCaseApi } from './post_case'; +import { initPostCommentApi } from './post_comment'; +import { initUpdateCaseApi } from './update_case'; +import { initUpdateCommentApi } from './update_comment'; +import { CaseServiceSetup } from '../../services'; + +export interface RouteDeps { + caseService: CaseServiceSetup; + router: IRouter; +} + +export function initCaseApi(deps: RouteDeps) { + initGetAllCaseCommentsApi(deps); + initGetAllCasesApi(deps); + initGetCaseApi(deps); + initGetCommentApi(deps); + initDeleteCaseApi(deps); + initDeleteCommentApi(deps); + initPostCaseApi(deps); + initPostCommentApi(deps); + initUpdateCaseApi(deps); + initUpdateCommentApi(deps); +} diff --git a/x-pack/plugins/case/server/routes/api/post_case.ts b/x-pack/plugins/case/server/routes/api/post_case.ts new file mode 100644 index 0000000000000..e5aa0a3548b48 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/post_case.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 { formatNewCase, wrapError } from './utils'; +import { NewCaseSchema } from './schema'; +import { RouteDeps } from '.'; + +export function initPostCaseApi({ caseService, router }: RouteDeps) { + router.post( + { + path: '/api/cases', + validate: { + body: NewCaseSchema, + }, + }, + async (context, request, response) => { + let createdBy; + try { + createdBy = await caseService.getUser({ request, response }); + } catch (error) { + return response.customError(wrapError(error)); + } + + try { + const newCase = await caseService.postNewCase({ + client: context.core.savedObjects.client, + attributes: formatNewCase(request.body, { + ...createdBy, + }), + }); + return response.ok({ body: newCase }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/post_comment.ts b/x-pack/plugins/case/server/routes/api/post_comment.ts new file mode 100644 index 0000000000000..3f4592f5bb11f --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/post_comment.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { formatNewComment, wrapError } from './utils'; +import { NewCommentSchema } from './schema'; +import { RouteDeps } from '.'; +import { CASE_SAVED_OBJECT } from '../../constants'; + +export function initPostCommentApi({ caseService, router }: RouteDeps) { + router.post( + { + path: '/api/cases/{id}/comment', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: NewCommentSchema, + }, + }, + async (context, request, response) => { + let createdBy; + let newComment; + try { + await caseService.getCase({ + client: context.core.savedObjects.client, + caseId: request.params.id, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + try { + createdBy = await caseService.getUser({ request, response }); + } catch (error) { + return response.customError(wrapError(error)); + } + try { + newComment = await caseService.postNewComment({ + client: context.core.savedObjects.client, + attributes: formatNewComment({ + newComment: request.body, + ...createdBy, + }), + references: [ + { + type: CASE_SAVED_OBJECT, + name: `associated-${CASE_SAVED_OBJECT}`, + id: request.params.id, + }, + ], + }); + + return response.ok({ body: newComment }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts new file mode 100644 index 0000000000000..4a4a0c3a11e36 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const UserSchema = schema.object({ + username: schema.string(), + full_name: schema.maybe(schema.string()), +}); + +export const NewCommentSchema = schema.object({ + comment: schema.string(), +}); + +export const CommentSchema = schema.object({ + comment: schema.string(), + created_at: schema.number(), + created_by: UserSchema, +}); + +export const UpdatedCommentSchema = schema.object({ + comment: schema.string(), +}); + +export const NewCaseSchema = schema.object({ + assignees: schema.arrayOf(UserSchema, { defaultValue: [] }), + description: schema.string(), + title: schema.string(), + state: schema.oneOf([schema.literal('open'), schema.literal('closed')], { defaultValue: 'open' }), + tags: schema.arrayOf(schema.string(), { defaultValue: [] }), + case_type: schema.string(), +}); + +export const UpdatedCaseSchema = schema.object({ + assignees: schema.maybe(schema.arrayOf(UserSchema)), + description: schema.maybe(schema.string()), + title: schema.maybe(schema.string()), + state: schema.maybe(schema.oneOf([schema.literal('open'), schema.literal('closed')])), + tags: schema.maybe(schema.arrayOf(schema.string())), + case_type: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts new file mode 100644 index 0000000000000..d943e4e5fd7dd --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { + CommentSchema, + NewCaseSchema, + NewCommentSchema, + UpdatedCaseSchema, + UpdatedCommentSchema, + UserSchema, +} from './schema'; + +export type NewCaseType = TypeOf; +export type NewCommentFormatted = TypeOf; +export type NewCommentType = TypeOf; +export type UpdatedCaseTyped = TypeOf; +export type UpdatedCommentType = TypeOf; +export type UserType = TypeOf; + +export interface NewCaseFormatted extends NewCaseType { + created_at: number; + created_by: UserType; +} + +export interface UpdatedCaseType { + assignees?: UpdatedCaseTyped['assignees']; + description?: UpdatedCaseTyped['description']; + title?: UpdatedCaseTyped['title']; + state?: UpdatedCaseTyped['state']; + tags?: UpdatedCaseTyped['tags']; + case_type?: UpdatedCaseTyped['case_type']; +} diff --git a/x-pack/plugins/case/server/routes/api/update_case.ts b/x-pack/plugins/case/server/routes/api/update_case.ts new file mode 100644 index 0000000000000..52c8cab0022dd --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/update_case.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapError } from './utils'; +import { RouteDeps } from '.'; +import { UpdatedCaseSchema } from './schema'; + +export function initUpdateCaseApi({ caseService, router }: RouteDeps) { + router.post( + { + path: '/api/cases/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: UpdatedCaseSchema, + }, + }, + async (context, request, response) => { + try { + const updatedCase = await caseService.updateCase({ + client: context.core.savedObjects.client, + caseId: request.params.id, + updatedAttributes: request.body, + }); + return response.ok({ body: updatedCase }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/update_comment.ts b/x-pack/plugins/case/server/routes/api/update_comment.ts new file mode 100644 index 0000000000000..e1ee6029e8e4f --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/update_comment.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapError } from './utils'; +import { NewCommentSchema } from './schema'; +import { RouteDeps } from '.'; + +export function initUpdateCommentApi({ caseService, router }: RouteDeps) { + router.post( + { + path: '/api/cases/comment/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: NewCommentSchema, + }, + }, + async (context, request, response) => { + try { + const updatedComment = await caseService.updateComment({ + client: context.core.savedObjects.client, + commentId: request.params.id, + updatedAttributes: request.body, + }); + return response.ok({ body: updatedComment }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts new file mode 100644 index 0000000000000..c6e33dbb8433b --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { boomify, isBoom } from 'boom'; +import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; +import { + NewCaseType, + NewCaseFormatted, + NewCommentType, + NewCommentFormatted, + UserType, +} from './types'; + +export const formatNewCase = ( + newCase: NewCaseType, + { full_name, username }: { full_name?: string; username: string } +): NewCaseFormatted => ({ + created_at: new Date().valueOf(), + created_by: { full_name, username }, + ...newCase, +}); + +interface NewCommentArgs { + newComment: NewCommentType; + full_name?: UserType['full_name']; + username: UserType['username']; +} +export const formatNewComment = ({ + newComment, + full_name, + username, +}: NewCommentArgs): NewCommentFormatted => ({ + ...newComment, + created_at: new Date().valueOf(), + created_by: { full_name, username }, +}); + +export function wrapError(error: any): CustomHttpResponseOptions { + const boom = isBoom(error) ? error : boomify(error); + return { + body: boom, + headers: boom.output.headers, + statusCode: boom.output.statusCode, + }; +} diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts new file mode 100644 index 0000000000000..684d905a5c71f --- /dev/null +++ b/x-pack/plugins/case/server/services/index.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + KibanaRequest, + KibanaResponseFactory, + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindResponse, + SavedObjectsUpdateResponse, + SavedObjectReference, +} from 'kibana/server'; +import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../constants'; +import { + NewCaseFormatted, + NewCommentFormatted, + UpdatedCaseType, + UpdatedCommentType, +} from '../routes/api/types'; +import { + AuthenticatedUser, + PluginSetupContract as SecurityPluginSetup, +} from '../../../security/server'; + +interface ClientArgs { + client: SavedObjectsClientContract; +} + +interface GetCaseArgs extends ClientArgs { + caseId: string; +} +interface GetCommentArgs extends ClientArgs { + commentId: string; +} +interface PostCaseArgs extends ClientArgs { + attributes: NewCaseFormatted; +} + +interface PostCommentArgs extends ClientArgs { + attributes: NewCommentFormatted; + references: SavedObjectReference[]; +} +interface UpdateCaseArgs extends ClientArgs { + caseId: string; + updatedAttributes: UpdatedCaseType; +} +interface UpdateCommentArgs extends ClientArgs { + commentId: string; + updatedAttributes: UpdatedCommentType; +} + +interface GetUserArgs { + request: KibanaRequest; + response: KibanaResponseFactory; +} + +interface CaseServiceDeps { + authentication: SecurityPluginSetup['authc']; +} +export interface CaseServiceSetup { + deleteCase(args: GetCaseArgs): Promise<{}>; + deleteComment(args: GetCommentArgs): Promise<{}>; + getAllCases(args: ClientArgs): Promise; + getAllCaseComments(args: GetCaseArgs): Promise; + getCase(args: GetCaseArgs): Promise; + getComment(args: GetCommentArgs): Promise; + getUser(args: GetUserArgs): Promise; + postNewCase(args: PostCaseArgs): Promise; + postNewComment(args: PostCommentArgs): Promise; + updateCase(args: UpdateCaseArgs): Promise; + updateComment(args: UpdateCommentArgs): Promise; +} + +export class CaseService { + constructor(private readonly log: Logger) {} + public setup = async ({ authentication }: CaseServiceDeps): Promise => ({ + deleteCase: async ({ client, caseId }: GetCaseArgs) => { + try { + this.log.debug(`Attempting to GET case ${caseId}`); + return await client.delete(CASE_SAVED_OBJECT, caseId); + } catch (error) { + this.log.debug(`Error on GET case ${caseId}: ${error}`); + throw error; + } + }, + deleteComment: async ({ client, commentId }: GetCommentArgs) => { + try { + this.log.debug(`Attempting to GET comment ${commentId}`); + return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId); + } catch (error) { + this.log.debug(`Error on GET comment ${commentId}: ${error}`); + throw error; + } + }, + getCase: async ({ client, caseId }: GetCaseArgs) => { + try { + this.log.debug(`Attempting to GET case ${caseId}`); + return await client.get(CASE_SAVED_OBJECT, caseId); + } catch (error) { + this.log.debug(`Error on GET case ${caseId}: ${error}`); + throw error; + } + }, + getComment: async ({ client, commentId }: GetCommentArgs) => { + try { + this.log.debug(`Attempting to GET comment ${commentId}`); + return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId); + } catch (error) { + this.log.debug(`Error on GET comment ${commentId}: ${error}`); + throw error; + } + }, + getAllCases: async ({ client }: ClientArgs) => { + try { + this.log.debug(`Attempting to GET all cases`); + return await client.find({ type: CASE_SAVED_OBJECT }); + } catch (error) { + this.log.debug(`Error on GET cases: ${error}`); + throw error; + } + }, + getAllCaseComments: async ({ client, caseId }: GetCaseArgs) => { + try { + this.log.debug(`Attempting to GET all comments for case ${caseId}`); + return await client.find({ + type: CASE_COMMENT_SAVED_OBJECT, + hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + }); + } catch (error) { + this.log.debug(`Error on GET all comments for case ${caseId}: ${error}`); + throw error; + } + }, + getUser: async ({ request, response }: GetUserArgs) => { + let user; + try { + this.log.debug(`Attempting to authenticate a user`); + user = await authentication!.getCurrentUser(request); + } catch (error) { + this.log.debug(`Error on GET user: ${error}`); + throw error; + } + if (!user) { + this.log.debug(`Error on GET user: Bad User`); + throw new Error('Bad User - the user is not authenticated'); + } + return user; + }, + postNewCase: async ({ client, attributes }: PostCaseArgs) => { + try { + this.log.debug(`Attempting to POST a new case`); + return await client.create(CASE_SAVED_OBJECT, { ...attributes }); + } catch (error) { + this.log.debug(`Error on POST a new case: ${error}`); + throw error; + } + }, + postNewComment: async ({ client, attributes, references }: PostCommentArgs) => { + try { + this.log.debug(`Attempting to POST a new comment`); + return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references }); + } catch (error) { + this.log.debug(`Error on POST a new comment: ${error}`); + throw error; + } + }, + updateCase: async ({ client, caseId, updatedAttributes }: UpdateCaseArgs) => { + try { + this.log.debug(`Attempting to UPDATE case ${caseId}`); + return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }); + } catch (error) { + this.log.debug(`Error on UPDATE case ${caseId}: ${error}`); + throw error; + } + }, + updateComment: async ({ client, commentId, updatedAttributes }: UpdateCommentArgs) => { + try { + this.log.debug(`Attempting to UPDATE comment ${commentId}`); + return await client.update(CASE_COMMENT_SAVED_OBJECT, commentId, { + ...updatedAttributes, + }); + } catch (error) { + this.log.debug(`Error on UPDATE comment ${commentId}: ${error}`); + throw error; + } + }, + }); +} diff --git a/x-pack/plugins/endpoint/server/config.test.ts b/x-pack/plugins/endpoint/server/config.test.ts new file mode 100644 index 0000000000000..39f6bca2d43ca --- /dev/null +++ b/x-pack/plugins/endpoint/server/config.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EndpointConfigSchema, EndpointConfigType } from './config'; + +describe('test config schema', () => { + it('test config defaults', () => { + const config: EndpointConfigType = EndpointConfigSchema.validate({}); + expect(config.enabled).toEqual(false); + expect(config.endpointResultListDefaultPageSize).toEqual(10); + expect(config.endpointResultListDefaultFirstPageIndex).toEqual(0); + }); +}); diff --git a/x-pack/plugins/endpoint/server/config.ts b/x-pack/plugins/endpoint/server/config.ts new file mode 100644 index 0000000000000..3f9a8a5508dd8 --- /dev/null +++ b/x-pack/plugins/endpoint/server/config.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; +import { Observable } from 'rxjs'; +import { PluginInitializerContext } from 'kibana/server'; + +export type EndpointConfigType = ReturnType extends Observable + ? P + : ReturnType; + +export const EndpointConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + endpointResultListDefaultFirstPageIndex: schema.number({ defaultValue: 0 }), + endpointResultListDefaultPageSize: schema.number({ defaultValue: 10 }), +}); + +export function createConfig$(context: PluginInitializerContext) { + return context.config.create>(); +} diff --git a/x-pack/plugins/endpoint/server/index.ts b/x-pack/plugins/endpoint/server/index.ts index eec836141ea5e..ae603b7e44449 100644 --- a/x-pack/plugins/endpoint/server/index.ts +++ b/x-pack/plugins/endpoint/server/index.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; -import { PluginInitializer } from 'src/core/server'; +import { PluginInitializer, PluginInitializerContext } from 'src/core/server'; import { EndpointPlugin, EndpointPluginStart, @@ -13,9 +12,10 @@ import { EndpointPluginStartDependencies, EndpointPluginSetupDependencies, } from './plugin'; +import { EndpointConfigSchema } from './config'; export const config = { - schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + schema: EndpointConfigSchema, }; export const plugin: PluginInitializer< @@ -23,4 +23,4 @@ export const plugin: PluginInitializer< EndpointPluginStart, EndpointPluginSetupDependencies, EndpointPluginStartDependencies -> = () => new EndpointPlugin(); +> = (initializerContext: PluginInitializerContext) => new EndpointPlugin(initializerContext); diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts new file mode 100644 index 0000000000000..87d373d3a4f34 --- /dev/null +++ b/x-pack/plugins/endpoint/server/plugin.test.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 { CoreSetup } from 'kibana/server'; +import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; +import { coreMock } from '../../../../src/core/server/mocks'; +import { PluginSetupContract } from '../../features/server'; + +describe('test endpoint plugin', () => { + let plugin: EndpointPlugin; + let mockCoreSetup: MockedKeys; + let mockedEndpointPluginSetupDependencies: jest.Mocked; + let mockedPluginSetupContract: jest.Mocked; + beforeEach(() => { + plugin = new EndpointPlugin( + coreMock.createPluginInitializerContext({ + cookieName: 'sid', + sessionTimeout: 1500, + }) + ); + + mockCoreSetup = coreMock.createSetup(); + mockedPluginSetupContract = { + registerFeature: jest.fn(), + getFeatures: jest.fn(), + getFeaturesUICapabilities: jest.fn(), + registerLegacyAPI: jest.fn(), + }; + mockedEndpointPluginSetupDependencies = { features: mockedPluginSetupContract }; + }); + + it('test properly setup plugin', async () => { + await plugin.setup(mockCoreSetup, mockedEndpointPluginSetupDependencies); + expect(mockedPluginSetupContract.registerFeature).toBeCalledTimes(1); + expect(mockCoreSetup.http.createRouter).toBeCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index b41dfee1f78fd..7ed116ba21140 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -3,9 +3,13 @@ * 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, CoreSetup } from 'kibana/server'; +import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; +import { first } from 'rxjs/operators'; import { addRoutes } from './routes'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; +import { createConfig$, EndpointConfigType } from './config'; +import { EndpointAppContext } from './types'; +import { registerEndpointRoutes } from './routes/endpoints'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; @@ -23,6 +27,10 @@ export class EndpointPlugin EndpointPluginSetupDependencies, EndpointPluginStartDependencies > { + private readonly logger: Logger; + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get('endpoint'); + } public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { plugins.features.registerFeature({ id: 'endpoint', @@ -49,10 +57,23 @@ export class EndpointPlugin }, }, }); + const endpointContext = { + logFactory: this.initializerContext.logger, + config: (): Promise => { + return createConfig$(this.initializerContext) + .pipe(first()) + .toPromise(); + }, + } as EndpointAppContext; const router = core.http.createRouter(); addRoutes(router); + registerEndpointRoutes(router, endpointContext); } - public start() {} - public stop() {} + public start() { + this.logger.debug('Starting plugin'); + } + public stop() { + this.logger.debug('Stopping plugin'); + } } diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts new file mode 100644 index 0000000000000..60433f86b6f7e --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + IClusterClient, + IRouter, + IScopedClusterClient, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from 'kibana/server'; +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../src/core/server/mocks'; +import { EndpointData } from '../types'; +import { SearchResponse } from 'elasticsearch'; +import { EndpointResultList, registerEndpointRoutes } from './endpoints'; +import { EndpointConfigSchema } from '../config'; +import * as data from '../test_data/all_endpoints_data.json'; + +describe('test endpoint route', () => { + let routerMock: jest.Mocked; + let mockResponse: jest.Mocked; + let mockClusterClient: jest.Mocked; + let mockScopedClient: jest.Mocked; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + beforeEach(() => { + mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< + IClusterClient + >; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClient); + routerMock = httpServiceMock.createRouter(); + mockResponse = httpServerMock.createResponseFactory(); + registerEndpointRoutes(routerMock, { + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }); + }); + + it('test find the latest of all endpoints', async () => { + const mockRequest = httpServerMock.createKibanaRequest({}); + + const response: SearchResponse = (data as unknown) as SearchResponse< + EndpointData + >; + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/endpoints') + )!; + + await routeHandler( + ({ + core: { + elasticsearch: { + dataClient: mockScopedClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; + expect(endpointResultList.endpoints.length).toEqual(3); + expect(endpointResultList.total).toEqual(3); + expect(endpointResultList.request_index).toEqual(0); + expect(endpointResultList.request_page_size).toEqual(10); + }); + + it('test find the latest of all endpoints with params', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + paging_properties: [ + { + page_size: 10, + }, + { + page_index: 1, + }, + ], + }, + }); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve((data as unknown) as SearchResponse) + ); + [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/endpoints') + )!; + + await routeHandler( + ({ + core: { + elasticsearch: { + dataClient: mockScopedClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; + expect(endpointResultList.endpoints.length).toEqual(3); + expect(endpointResultList.total).toEqual(3); + expect(endpointResultList.request_index).toEqual(10); + expect(endpointResultList.request_page_size).toEqual(10); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts new file mode 100644 index 0000000000000..9d2babc61f11f --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; +import { schema } from '@kbn/config-schema'; +import { EndpointAppContext, EndpointData } from '../types'; +import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders'; + +interface HitSource { + _source: EndpointData; +} + +export interface EndpointResultList { + // the endpoint restricted by the page size + endpoints: EndpointData[]; + // the total number of unique endpoints in the index + total: number; + // the page size requested + request_page_size: number; + // the index requested + request_index: number; +} + +export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { + router.post( + { + path: '/api/endpoint/endpoints', + validate: { + body: schema.nullable( + schema.object({ + paging_properties: schema.arrayOf( + schema.oneOf([ + // the number of results to return for this request per page + schema.object({ + page_size: schema.number({ defaultValue: 10, min: 1, max: 10000 }), + }), + // the index of the page to return + schema.object({ page_index: schema.number({ defaultValue: 0, min: 0 }) }), + ]) + ), + }) + ), + }, + options: { authRequired: true }, + }, + async (context, req, res) => { + try { + const queryParams = await kibanaRequestToEndpointListQuery(req, endpointAppContext); + const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( + 'search', + queryParams + )) as SearchResponse; + return res.ok({ body: mapToEndpointResultList(queryParams, response) }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); +} + +function mapToEndpointResultList( + queryParams: Record, + searchResponse: SearchResponse +): EndpointResultList { + const totalNumberOfEndpoints = searchResponse?.aggregations?.total?.value || 0; + if (searchResponse.hits.hits.length > 0) { + return { + request_page_size: queryParams.size, + request_index: queryParams.from, + endpoints: searchResponse.hits.hits + .map(response => response.inner_hits.most_recent.hits.hits) + .flatMap(data => data as HitSource) + .map(entry => entry._source), + total: totalNumberOfEndpoints, + }; + } else { + return { + request_page_size: queryParams.size, + request_index: queryParams.from, + total: totalNumberOfEndpoints, + endpoints: [], + }; + } +} diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts new file mode 100644 index 0000000000000..2a8cecec16526 --- /dev/null +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; +import { EndpointConfigSchema } from '../../config'; +import { kibanaRequestToEndpointListQuery } from './endpoint_query_builders'; + +describe('test query builder', () => { + describe('test query builder request processing', () => { + it('test default query params for all endpoints when no params or body is provided', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + }); + const query = await kibanaRequestToEndpointListQuery(mockRequest, { + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }); + expect(query).toEqual({ + body: { + query: { + match_all: {}, + }, + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + from: 0, + size: 10, + index: 'endpoint-agent*', + } as Record); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts new file mode 100644 index 0000000000000..7430ba9721608 --- /dev/null +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaRequest } from 'kibana/server'; +import { EndpointAppConstants, EndpointAppContext } from '../../types'; + +export const kibanaRequestToEndpointListQuery = async ( + request: KibanaRequest, + endpointAppContext: EndpointAppContext +): Promise> => { + const pagingProperties = await getPagingProperties(request, endpointAppContext); + return { + body: { + query: { + match_all: {}, + }, + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + from: pagingProperties.pageIndex * pagingProperties.pageSize, + size: pagingProperties.pageSize, + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + }; +}; + +async function getPagingProperties( + request: KibanaRequest, + endpointAppContext: EndpointAppContext +) { + const config = await endpointAppContext.config(); + const pagingProperties: { page_size?: number; page_index?: number } = {}; + if (request?.body?.paging_properties) { + for (const property of request.body.paging_properties) { + Object.assign( + pagingProperties, + ...Object.keys(property).map(key => ({ [key]: property[key] })) + ); + } + } + return { + pageSize: pagingProperties.page_size || config.endpointResultListDefaultPageSize, + pageIndex: pagingProperties.page_index || config.endpointResultListDefaultFirstPageIndex, + }; +} diff --git a/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json b/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json new file mode 100644 index 0000000000000..d505b2c929828 --- /dev/null +++ b/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json @@ -0,0 +1,348 @@ +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 9, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "UV_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "606267a9-2e51-42b4-956e-6cc7812e3447", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "natalee-2", + "hostname": "natalee-2.example.com", + "ip": "10.5.220.127", + "mac_address": "17-5f-c9-f8-ca-d6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=natalee-2,DC=example,DC=com", + "active_directory_hostname": "natalee-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "fields": { + "machine_id": [ + "606267a9-2e51-42b4-956e-6cc7812e3447" + ] + }, + "sort": [ + 1577477368377 + ], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 3, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "UV_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "606267a9-2e51-42b4-956e-6cc7812e3447", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "natalee-2", + "hostname": "natalee-2.example.com", + "ip": "10.5.220.127", + "mac_address": "17-5f-c9-f8-ca-d6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=natalee-2,DC=example,DC=com", + "active_directory_hostname": "natalee-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "sort": [ + 1577477368377 + ] + } + ] + } + } + } + }, + { + "_index": "endpoint-agent", + "_id": "Ul_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "8ec625e1-a80c-4c9f-bdfd-496060aa6310", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "luttrell-2", + "hostname": "luttrell-2.example.com", + "ip": "10.246.84.193", + "mac_address": "dc-d-88-14-c3-c6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=luttrell-2,DC=example,DC=com", + "active_directory_hostname": "luttrell-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "Default", + "id": "00000000-0000-0000-0000-000000000000" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "fields": { + "machine_id": [ + "8ec625e1-a80c-4c9f-bdfd-496060aa6310" + ] + }, + "sort": [ + 1577477368377 + ], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 3, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "Ul_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "8ec625e1-a80c-4c9f-bdfd-496060aa6310", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "luttrell-2", + "hostname": "luttrell-2.example.com", + "ip": "10.246.84.193", + "mac_address": "dc-d-88-14-c3-c6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=luttrell-2,DC=example,DC=com", + "active_directory_hostname": "luttrell-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "Default", + "id": "00000000-0000-0000-0000-000000000000" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "sort": [ + 1577477368377 + ] + } + ] + } + } + } + }, + { + "_index": "endpoint-agent", + "_id": "U1_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "853a308c-6e6d-4b92-a32b-2f623b6c8cf4", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "akeylah-7", + "hostname": "akeylah-7.example.com", + "ip": "10.252.242.44", + "mac_address": "27-b9-51-21-31-a", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=akeylah-7,DC=example,DC=com", + "active_directory_hostname": "akeylah-7.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "fields": { + "machine_id": [ + "853a308c-6e6d-4b92-a32b-2f623b6c8cf4" + ] + }, + "sort": [ + 1577477368377 + ], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 3, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "U1_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "853a308c-6e6d-4b92-a32b-2f623b6c8cf4", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "akeylah-7", + "hostname": "akeylah-7.example.com", + "ip": "10.252.242.44", + "mac_address": "27-b9-51-21-31-a", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=akeylah-7,DC=example,DC=com", + "active_directory_hostname": "akeylah-7.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "sort": [ + 1577477368377 + ] + } + ] + } + } + } + } + ] + }, + "aggregations": { + "total": { + "value": 3 + } + } +} diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts new file mode 100644 index 0000000000000..c6d0e3dea70cf --- /dev/null +++ b/x-pack/plugins/endpoint/server/types.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LoggerFactory } from 'kibana/server'; +import { EndpointConfigType } from './config'; + +export interface EndpointAppContext { + logFactory: LoggerFactory; + config(): Promise; +} + +export class EndpointAppConstants { + static ENDPOINT_INDEX_NAME = 'endpoint-agent*'; +} + +export interface EndpointData { + machine_id: string; + created_at: Date; + host: { + name: string; + hostname: string; + ip: string; + mac_address: string; + os: { + name: string; + full: string; + }; + }; + endpoint: { + domain: string; + is_base_image: boolean; + active_directory_distinguished_name: string; + active_directory_hostname: string; + upgrade: { + status?: string; + updated_at?: Date; + }; + isolation: { + status: boolean; + request_status?: string | boolean; + updated_at?: Date; + }; + policy: { + name: string; + id: string; + }; + sensor: { + persistence: boolean; + status: object; + }; + }; +} diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 0a5b9f62f12a1..125378891151b 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -71,7 +71,11 @@ export function registerExploreRoute({ throw Boom.badRequest(relevantCause.reason); } - throw Boom.boomify(error); + return response.internalError({ + body: { + message: error.message, + }, + }); } } ) diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 400cdc4e82b6e..91b404dc7cb91 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -6,7 +6,6 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; -import Boom from 'boom'; import { LicenseState, verifyApiAccess } from '../lib/license_state'; export function registerSearchRoute({ @@ -53,7 +52,12 @@ export function registerSearchRoute({ }, }); } catch (error) { - throw Boom.boomify(error, { statusCode: error.statusCode || 500 }); + return response.customError({ + statusCode: error.statusCode || 500, + body: { + message: error.message, + }, + }); } } ) diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index 0b5a3533bd3b6..9547a2dc52966 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BehaviorSubject } from 'rxjs'; import { take, toArray } from 'rxjs/operators'; import moment from 'moment'; import { LicenseType } from '../common/types'; @@ -53,7 +52,7 @@ describe('licensing plugin', () => { features: {}, }); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { license$ } = await plugin.setup(coreSetup); const license = await license$.pipe(take(1)).toPromise(); @@ -71,7 +70,7 @@ describe('licensing plugin', () => { }) ); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { license$ } = await plugin.setup(coreSetup); const [first, second, third] = await license$.pipe(take(3), toArray()).toPromise(); @@ -85,7 +84,7 @@ describe('licensing plugin', () => { const dataClient = elasticsearchServiceMock.createClusterClient(); dataClient.callAsInternalUser.mockRejectedValue(new Error('test')); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { license$ } = await plugin.setup(coreSetup); const license = await license$.pipe(take(1)).toPromise(); @@ -99,7 +98,7 @@ describe('licensing plugin', () => { error.status = 400; dataClient.callAsInternalUser.mockRejectedValue(error); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { license$ } = await plugin.setup(coreSetup); const license = await license$.pipe(take(1)).toPromise(); @@ -119,7 +118,7 @@ describe('licensing plugin', () => { .mockResolvedValue({ license: buildRawLicense(), features: {} }); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { license$ } = await plugin.setup(coreSetup); const [first, second, third] = await license$.pipe(take(3), toArray()).toPromise(); @@ -137,7 +136,7 @@ describe('licensing plugin', () => { }); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; await plugin.setup(coreSetup); await flushPromises(); @@ -152,7 +151,7 @@ describe('licensing plugin', () => { }); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; await plugin.setup(coreSetup); await flushPromises(); @@ -180,7 +179,7 @@ describe('licensing plugin', () => { ); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { license$ } = await plugin.setup(coreSetup); const [first, second, third] = await license$.pipe(take(3), toArray()).toPromise(); @@ -209,7 +208,7 @@ describe('licensing plugin', () => { features: {}, }); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { refresh } = await plugin.setup(coreSetup); expect(dataClient.callAsInternalUser).toHaveBeenCalledTimes(0); @@ -242,7 +241,7 @@ describe('licensing plugin', () => { features: {}, }); const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + coreSetup.elasticsearch.dataClient = dataClient; const { createLicensePoller, license$ } = await plugin.setup(coreSetup); const customClient = elasticsearchServiceMock.createClusterClient(); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 2eabd534a997c..383245e6f4ee8 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -92,7 +92,7 @@ export class LicensingPlugin implements Plugin { this.logger.debug('Setting up Licensing plugin'); const config = await this.config$.pipe(take(1)).toPromise(); const pollingFrequency = config.api_polling_frequency; - const dataClient = await core.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + const dataClient = await core.elasticsearch.dataClient; const { refresh, license$ } = this.createLicensePoller( dataClient, diff --git a/x-pack/plugins/security/common/licensing/license_features.ts b/x-pack/plugins/security/common/licensing/license_features.ts index 6b6c86d48c21e..33f8370a1b43e 100644 --- a/x-pack/plugins/security/common/licensing/license_features.ts +++ b/x-pack/plugins/security/common/licensing/license_features.ts @@ -23,6 +23,11 @@ export interface SecurityLicenseFeatures { */ readonly showLinks: boolean; + /** + * Indicates whether we show the Role Mappings UI. + */ + readonly showRoleMappingsManagement: boolean; + /** * Indicates whether we allow users to define document level security in roles. */ diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index f4fa5e00e2387..df2d66a036039 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -17,6 +17,7 @@ describe('license features', function() { showLogin: true, allowLogin: false, showLinks: false, + showRoleMappingsManagement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, layout: 'error-es-unavailable', @@ -34,6 +35,7 @@ describe('license features', function() { showLogin: true, allowLogin: false, showLinks: false, + showRoleMappingsManagement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, layout: 'error-xpack-unavailable', @@ -63,6 +65,7 @@ describe('license features', function() { "layout": "error-xpack-unavailable", "showLinks": false, "showLogin": true, + "showRoleMappingsManagement": false, }, ] `); @@ -79,6 +82,7 @@ describe('license features', function() { "linksMessage": "Access is denied because Security is disabled in Elasticsearch.", "showLinks": false, "showLogin": false, + "showRoleMappingsManagement": false, }, ] `); @@ -87,10 +91,12 @@ describe('license features', function() { } }); - it('should show login page and other security elements, allow RBAC but forbid document level security if license is not platinum or trial.', () => { - const mockRawLicense = licensingMock.createLicenseMock(); - mockRawLicense.hasAtLeast.mockReturnValue(false); - mockRawLicense.getFeature.mockReturnValue({ isEnabled: true, isAvailable: true }); + it('should show login page and other security elements, allow RBAC but forbid role mappings and document level security if license is basic.', () => { + const mockRawLicense = licensingMock.createLicense({ + features: { security: { isEnabled: true, isAvailable: true } }, + }); + + const getFeatureSpy = jest.spyOn(mockRawLicense, 'getFeature'); const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), @@ -99,18 +105,19 @@ describe('license features', function() { showLogin: true, allowLogin: true, showLinks: true, + showRoleMappingsManagement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: true, }); - expect(mockRawLicense.getFeature).toHaveBeenCalledTimes(1); - expect(mockRawLicense.getFeature).toHaveBeenCalledWith('security'); + expect(getFeatureSpy).toHaveBeenCalledTimes(1); + expect(getFeatureSpy).toHaveBeenCalledWith('security'); }); it('should not show login page or other security elements if security is disabled in Elasticsearch.', () => { - const mockRawLicense = licensingMock.createLicenseMock(); - mockRawLicense.hasAtLeast.mockReturnValue(false); - mockRawLicense.getFeature.mockReturnValue({ isEnabled: false, isAvailable: true }); + const mockRawLicense = licensingMock.createLicense({ + features: { security: { isEnabled: false, isAvailable: true } }, + }); const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), @@ -119,6 +126,7 @@ describe('license features', function() { showLogin: false, allowLogin: false, showLinks: false, + showRoleMappingsManagement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -126,12 +134,31 @@ describe('license features', function() { }); }); - it('should allow to login, allow RBAC and document level security if license >= platinum', () => { - const mockRawLicense = licensingMock.createLicenseMock(); - mockRawLicense.hasAtLeast.mockImplementation(license => { - return license === 'trial' || license === 'platinum' || license === 'enterprise'; + it('should allow role mappings, but not DLS/FLS if license = gold', () => { + const mockRawLicense = licensingMock.createLicense({ + license: { mode: 'gold', type: 'gold' }, + features: { security: { isEnabled: true, isAvailable: true } }, + }); + + const serviceSetup = new SecurityLicenseService().setup({ + license$: of(mockRawLicense), + }); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: true, + allowLogin: true, + showLinks: true, + showRoleMappingsManagement: true, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + allowRbac: true, + }); + }); + + it('should allow to login, allow RBAC, allow role mappings, and document level security if license >= platinum', () => { + const mockRawLicense = licensingMock.createLicense({ + license: { mode: 'platinum', type: 'platinum' }, + features: { security: { isEnabled: true, isAvailable: true } }, }); - mockRawLicense.getFeature.mockReturnValue({ isEnabled: true, isAvailable: true }); const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), @@ -140,6 +167,7 @@ describe('license features', function() { showLogin: true, allowLogin: true, showLinks: true, + showRoleMappingsManagement: true, allowRoleDocumentLevelSecurity: true, allowRoleFieldLevelSecurity: true, allowRbac: true, diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts index 0f9da03f9f6ec..e6d2eff49ed0d 100644 --- a/x-pack/plugins/security/common/licensing/license_service.ts +++ b/x-pack/plugins/security/common/licensing/license_service.ts @@ -70,6 +70,7 @@ export class SecurityLicenseService { showLogin: true, allowLogin: false, showLinks: false, + showRoleMappingsManagement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -85,6 +86,7 @@ export class SecurityLicenseService { showLogin: false, allowLogin: false, showLinks: false, + showRoleMappingsManagement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -92,11 +94,13 @@ export class SecurityLicenseService { }; } + const showRoleMappingsManagement = rawLicense.hasAtLeast('gold'); const isLicensePlatinumOrBetter = rawLicense.hasAtLeast('platinum'); return { showLogin: true, allowLogin: true, showLinks: true, + showRoleMappingsManagement, // Only platinum and trial licenses are compliant with field- and document-level security. allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter, allowRoleFieldLevelSecurity: isLicensePlatinumOrBetter, diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 226ea3b70afe2..f3c65ed7e3cf1 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -12,3 +12,10 @@ export { FeaturesPrivileges } from './features_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; export { KibanaPrivileges } from './kibana_privileges'; +export { + InlineRoleTemplate, + StoredRoleTemplate, + InvalidRoleTemplate, + RoleTemplate, + RoleMapping, +} from './role_mapping'; diff --git a/x-pack/plugins/security/common/model/role_mapping.ts b/x-pack/plugins/security/common/model/role_mapping.ts new file mode 100644 index 0000000000000..99de183f648f7 --- /dev/null +++ b/x-pack/plugins/security/common/model/role_mapping.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface RoleMappingAnyRule { + any: RoleMappingRule[]; +} + +interface RoleMappingAllRule { + all: RoleMappingRule[]; +} + +interface RoleMappingFieldRule { + field: Record; +} + +interface RoleMappingExceptRule { + except: RoleMappingRule; +} + +type RoleMappingRule = + | RoleMappingAnyRule + | RoleMappingAllRule + | RoleMappingFieldRule + | RoleMappingExceptRule; + +type RoleTemplateFormat = 'string' | 'json'; + +export interface InlineRoleTemplate { + template: { source: string }; + format?: RoleTemplateFormat; +} + +export interface StoredRoleTemplate { + template: { id: string }; + format?: RoleTemplateFormat; +} + +export interface InvalidRoleTemplate { + template: string; + format?: RoleTemplateFormat; +} + +export type RoleTemplate = InlineRoleTemplate | StoredRoleTemplate | InvalidRoleTemplate; + +export interface RoleMapping { + name: string; + enabled: boolean; + roles?: string[]; + role_templates?: RoleTemplate[]; + rules: RoleMappingRule | {}; + metadata: Record; +} diff --git a/x-pack/plugins/security/server/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch_client_plugin.ts index 60d947bd65863..996dcb685f29b 100644 --- a/x-pack/plugins/security/server/elasticsearch_client_plugin.ts +++ b/x-pack/plugins/security/server/elasticsearch_client_plugin.ts @@ -573,4 +573,64 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen fmt: '/_security/delegate_pki', }, }); + + /** + * Retrieves all configured role mappings. + * + * @returns {{ [roleMappingName]: { enabled: boolean; roles: string[]; rules: Record} }} + */ + shield.getRoleMappings = ca({ + method: 'GET', + urls: [ + { + fmt: '/_security/role_mapping', + }, + { + fmt: '/_security/role_mapping/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + ], + }); + + /** + * Saves the specified role mapping. + */ + shield.saveRoleMapping = ca({ + method: 'POST', + needBody: true, + urls: [ + { + fmt: '/_security/role_mapping/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + ], + }); + + /** + * Deletes the specified role mapping. + */ + shield.deleteRoleMapping = ca({ + method: 'DELETE', + urls: [ + { + fmt: '/_security/role_mapping/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + ], + }); } diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 33f554be5caa3..17e49b8cf40d3 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -17,6 +17,7 @@ import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. export { + Authentication, AuthenticationResult, DeauthenticationResult, CreateAPIKeyResult, @@ -24,6 +25,7 @@ export { InvalidateAPIKeyResult, } from './authentication'; export { PluginSetupContract }; +export { AuthenticatedUser } from '../common/model'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 05d67b112bad8..5e32a0e90198a 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import { IClusterClient, CoreSetup } from '../../../../src/core/server'; +import { ICustomClusterClient, CoreSetup } from '../../../../src/core/server'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { Plugin, PluginSetupDependencies } from './plugin'; @@ -15,7 +15,7 @@ import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/ describe('Security Plugin', () => { let plugin: Plugin; let mockCoreSetup: MockedKeys; - let mockClusterClient: jest.Mocked; + let mockClusterClient: jest.Mocked; let mockDependencies: PluginSetupDependencies; beforeEach(() => { plugin = new Plugin( @@ -35,9 +35,9 @@ describe('Security Plugin', () => { mockCoreSetup = coreMock.createSetup(); mockCoreSetup.http.isTlsEnabled = true; - mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); mockCoreSetup.elasticsearch.createClient.mockReturnValue( - (mockClusterClient as unknown) as jest.Mocked + (mockClusterClient as unknown) as jest.Mocked ); mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 9c4b01f94ef4d..ce682d8b30eb7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -7,7 +7,7 @@ import { combineLatest } from 'rxjs'; import { first } from 'rxjs/operators'; import { - IClusterClient, + ICustomClusterClient, CoreSetup, KibanaRequest, Logger, @@ -85,7 +85,7 @@ export interface PluginSetupDependencies { */ export class Plugin { private readonly logger: Logger; - private clusterClient?: IClusterClient; + private clusterClient?: ICustomClusterClient; private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index ade840e7ca495..01df67cacb800 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -14,6 +14,7 @@ import { defineAuthorizationRoutes } from './authorization'; import { defineApiKeysRoutes } from './api_keys'; import { defineIndicesRoutes } from './indices'; import { defineUsersRoutes } from './users'; +import { defineRoleMappingRoutes } from './role_mapping'; /** * Describes parameters used to define HTTP routes. @@ -35,4 +36,5 @@ export function defineRoutes(params: RouteDefinitionParams) { defineApiKeysRoutes(params); defineIndicesRoutes(params); defineUsersRoutes(params); + defineRoleMappingRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts new file mode 100644 index 0000000000000..e8a8a7216330b --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { routeDefinitionParamsMock } from '../index.mock'; +import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { defineRoleMappingDeleteRoutes } from './delete'; + +describe('DELETE role mappings', () => { + it('allows a role mapping to be deleted', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ acknowledged: true }); + + defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; + + const name = 'mapping1'; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `/internal/security/role_mapping/${name}`, + params: { name }, + headers, + }); + const mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ state: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(response.payload).toEqual({ acknowledged: true }); + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect( + mockScopedClusterClient.callAsCurrentUser + ).toHaveBeenCalledWith('shield.deleteRoleMapping', { name }); + }); + + describe('failure', () => { + it('returns result of license check', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; + + const name = 'mapping1'; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `/internal/security/role_mapping/${name}`, + params: { name }, + headers, + }); + const mockContext = ({ + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: LICENSE_CHECK_STATE.Invalid, + message: 'test forbidden message', + }), + }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(403); + expect(response.payload).toEqual({ message: 'test forbidden message' }); + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.ts new file mode 100644 index 0000000000000..dc11bcd914b35 --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.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 { schema } from '@kbn/config-schema'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { wrapError } from '../../errors'; +import { RouteDefinitionParams } from '..'; + +export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { + const { clusterClient, router } = params; + + router.delete( + { + path: '/internal/security/role_mapping/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const deleteResponse = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.deleteRoleMapping', { + name: request.params.name, + }); + return response.ok({ body: deleteResponse }); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts new file mode 100644 index 0000000000000..f2c48fd370434 --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { routeDefinitionParamsMock } from '../index.mock'; +import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { + kibanaResponseFactory, + RequestHandlerContext, + IClusterClient, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { defineRoleMappingFeatureCheckRoute } from './feature_check'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + canManageRoleMappings?: boolean; + nodeSettingsResponse?: Record; + xpackUsageResponse?: Record; + internalUserClusterClientImpl?: IClusterClient['callAsInternalUser']; + asserts: { statusCode: number; result?: Record }; +} + +const defaultXpackUsageResponse = { + security: { + realms: { + native: { + available: true, + enabled: true, + }, + pki: { + available: true, + enabled: true, + }, + }, + }, +}; + +const getDefaultInternalUserClusterClientImpl = ( + nodeSettingsResponse: TestOptions['nodeSettingsResponse'], + xpackUsageResponse: TestOptions['xpackUsageResponse'] +) => + ((async (endpoint: string, clientParams: Record) => { + if (!clientParams) throw new TypeError('expected clientParams'); + + if (endpoint === 'nodes.info') { + return nodeSettingsResponse; + } + + if (endpoint === 'transport.request') { + if (clientParams.path === '/_xpack/usage') { + return xpackUsageResponse; + } + } + + throw new Error(`unexpected endpoint: ${endpoint}`); + }) as unknown) as TestOptions['internalUserClusterClientImpl']; + +describe('GET role mappings feature check', () => { + const getFeatureCheckTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + canManageRoleMappings = true, + nodeSettingsResponse = {}, + xpackUsageResponse = defaultXpackUsageResponse, + internalUserClusterClientImpl = getDefaultInternalUserClusterClientImpl( + nodeSettingsResponse, + xpackUsageResponse + ), + asserts, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation( + internalUserClusterClientImpl + ); + + mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method, payload) => { + if (method === 'shield.hasPrivileges') { + return { + has_all_requested: canManageRoleMappings, + }; + } + }); + + defineRoleMappingFeatureCheckRoute(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: `/internal/security/_check_role_mapping_features`, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + getFeatureCheckTest('allows both script types with the default settings', { + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + hasCompatibleRealms: true, + }, + }, + }); + + getFeatureCheckTest('allows both script types when explicitly enabled', { + nodeSettingsResponse: { + nodes: { + someNodeId: { + settings: { + script: { + allowed_types: ['stored', 'inline'], + }, + }, + }, + }, + }, + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + hasCompatibleRealms: true, + }, + }, + }); + + getFeatureCheckTest('disallows stored scripts when disabled', { + nodeSettingsResponse: { + nodes: { + someNodeId: { + settings: { + script: { + allowed_types: ['inline'], + }, + }, + }, + }, + }, + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: true, + canUseStoredScripts: false, + hasCompatibleRealms: true, + }, + }, + }); + + getFeatureCheckTest('disallows inline scripts when disabled', { + nodeSettingsResponse: { + nodes: { + someNodeId: { + settings: { + script: { + allowed_types: ['stored'], + }, + }, + }, + }, + }, + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: false, + canUseStoredScripts: true, + hasCompatibleRealms: true, + }, + }, + }); + + getFeatureCheckTest('indicates incompatible realms when only native and file are enabled', { + xpackUsageResponse: { + security: { + realms: { + native: { + available: true, + enabled: true, + }, + file: { + available: true, + enabled: true, + }, + }, + }, + }, + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + hasCompatibleRealms: false, + }, + }, + }); + + getFeatureCheckTest('indicates canManageRoleMappings=false for users without `manage_security`', { + canManageRoleMappings: false, + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: false, + }, + }, + }); + + getFeatureCheckTest( + 'falls back to allowing both script types if there is an error retrieving node settings', + { + internalUserClusterClientImpl: (() => { + return Promise.reject(new Error('something bad happened')); + }) as TestOptions['internalUserClusterClientImpl'], + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + hasCompatibleRealms: false, + }, + }, + } + ); +}); diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts new file mode 100644 index 0000000000000..2be4f4cd89177 --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger, IClusterClient } from 'src/core/server'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +interface NodeSettingsResponse { + nodes: { + [nodeId: string]: { + settings: { + script: { + allowed_types?: string[]; + allowed_contexts?: string[]; + }; + }; + }; + }; +} + +interface XPackUsageResponse { + security: { + realms: { + [realmName: string]: { + available: boolean; + enabled: boolean; + }; + }; + }; +} + +const INCOMPATIBLE_REALMS = ['file', 'native']; + +export function defineRoleMappingFeatureCheckRoute({ + router, + clusterClient, + logger, +}: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/_check_role_mapping_features', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + const { has_all_requested: canManageRoleMappings } = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.hasPrivileges', { + body: { + cluster: ['manage_security'], + }, + }); + + if (!canManageRoleMappings) { + return response.ok({ + body: { + canManageRoleMappings, + }, + }); + } + + const enabledFeatures = await getEnabledRoleMappingsFeatures(clusterClient, logger); + + return response.ok({ + body: { + ...enabledFeatures, + canManageRoleMappings, + }, + }); + }) + ); +} + +async function getEnabledRoleMappingsFeatures(clusterClient: IClusterClient, logger: Logger) { + logger.debug(`Retrieving role mappings features`); + + const nodeScriptSettingsPromise: Promise = clusterClient + .callAsInternalUser('nodes.info', { + filterPath: 'nodes.*.settings.script', + }) + .catch(error => { + // fall back to assuming that node settings are unset/at their default values. + // this will allow the role mappings UI to permit both role template script types, + // even if ES will disallow it at mapping evaluation time. + logger.error(`Error retrieving node settings for role mappings: ${error}`); + return {}; + }); + + const xpackUsagePromise: Promise = clusterClient + // `transport.request` is potentially unsafe when combined with untrusted user input. + // Do not augment with such input. + .callAsInternalUser('transport.request', { + method: 'GET', + path: '/_xpack/usage', + }) + .catch(error => { + // fall back to no external realms configured. + // this will cause a warning in the UI about no compatible realms being enabled, but will otherwise allow + // the mappings screen to function correctly. + logger.error(`Error retrieving XPack usage info for role mappings: ${error}`); + return { + security: { + realms: {}, + }, + } as XPackUsageResponse; + }); + + const [nodeScriptSettings, xpackUsage] = await Promise.all([ + nodeScriptSettingsPromise, + xpackUsagePromise, + ]); + + let canUseStoredScripts = true; + let canUseInlineScripts = true; + if (usesCustomScriptSettings(nodeScriptSettings)) { + canUseStoredScripts = Object.values(nodeScriptSettings.nodes).some(node => { + const allowedTypes = node.settings.script.allowed_types; + return !allowedTypes || allowedTypes.includes('stored'); + }); + + canUseInlineScripts = Object.values(nodeScriptSettings.nodes).some(node => { + const allowedTypes = node.settings.script.allowed_types; + return !allowedTypes || allowedTypes.includes('inline'); + }); + } + + const hasCompatibleRealms = Object.entries(xpackUsage.security.realms).some( + ([realmName, realm]) => { + return !INCOMPATIBLE_REALMS.includes(realmName) && realm.available && realm.enabled; + } + ); + + return { + hasCompatibleRealms, + canUseStoredScripts, + canUseInlineScripts, + }; +} + +function usesCustomScriptSettings( + nodeResponse: NodeSettingsResponse | {} +): nodeResponse is NodeSettingsResponse { + return nodeResponse.hasOwnProperty('nodes'); +} diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts new file mode 100644 index 0000000000000..c60d5518097ba --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { defineRoleMappingGetRoutes } from './get'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; + +const mockRoleMappingResponse = { + mapping1: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, + mapping2: { + enabled: true, + role_templates: [{ template: JSON.stringify({ source: 'foo_{{username}}' }) }], + rules: { + any: [ + { + field: { + dn: 'CN=admin,OU=example,O=com', + }, + }, + { + field: { + username: 'admin_*', + }, + }, + ], + }, + }, + mapping3: { + enabled: true, + role_templates: [{ template: 'template with invalid json' }], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, +}; + +describe('GET role mappings', () => { + it('returns all role mappings', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(mockRoleMappingResponse); + + defineRoleMappingGetRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: `/internal/security/role_mapping`, + headers, + }); + const mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ state: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(response.payload).toEqual([ + { + name: 'mapping1', + enabled: true, + roles: ['foo', 'bar'], + role_templates: [], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, + { + name: 'mapping2', + enabled: true, + role_templates: [{ template: { source: 'foo_{{username}}' } }], + rules: { + any: [ + { + field: { + dn: 'CN=admin,OU=example,O=com', + }, + }, + { + field: { + username: 'admin_*', + }, + }, + ], + }, + }, + { + name: 'mapping3', + enabled: true, + role_templates: [{ template: 'template with invalid json' }], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, + ]); + + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.getRoleMappings', + { name: undefined } + ); + }); + + it('returns role mapping by name', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ + mapping1: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, + }); + + defineRoleMappingGetRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const name = 'mapping1'; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: `/internal/security/role_mapping/${name}`, + params: { name }, + headers, + }); + const mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ state: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(response.payload).toEqual({ + name: 'mapping1', + enabled: true, + roles: ['foo', 'bar'], + role_templates: [], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }); + + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.getRoleMappings', + { name } + ); + }); + + describe('failure', () => { + it('returns result of license check', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + defineRoleMappingGetRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: `/internal/security/role_mapping`, + headers, + }); + const mockContext = ({ + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: LICENSE_CHECK_STATE.Invalid, + message: 'test forbidden message', + }), + }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(403); + expect(response.payload).toEqual({ message: 'test forbidden message' }); + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + }); + + it('returns a 404 when the role mapping is not found', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + Boom.notFound('role mapping not found!') + ); + + defineRoleMappingGetRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const name = 'mapping1'; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: `/internal/security/role_mapping/${name}`, + params: { name }, + headers, + }); + const mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ state: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(404); + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect( + mockScopedClusterClient.callAsCurrentUser + ).toHaveBeenCalledWith('shield.getRoleMappings', { name }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts new file mode 100644 index 0000000000000..9cd5cf83092e1 --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/get.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 { schema } from '@kbn/config-schema'; +import { RoleMapping } from '../../../../../legacy/plugins/security/common/model'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { wrapError } from '../../errors'; +import { RouteDefinitionParams } from '..'; + +interface RoleMappingsResponse { + [roleMappingName: string]: Omit; +} + +export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { + const { clusterClient, logger, router } = params; + + router.get( + { + path: '/internal/security/role_mapping/{name?}', + validate: { + params: schema.object({ + name: schema.maybe(schema.string()), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const expectSingleEntity = typeof request.params.name === 'string'; + + try { + const roleMappingsResponse: RoleMappingsResponse = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getRoleMappings', { + name: request.params.name, + }); + + const mappings = Object.entries(roleMappingsResponse).map(([name, mapping]) => { + return { + name, + ...mapping, + role_templates: (mapping.role_templates || []).map(entry => { + return { + ...entry, + template: tryParseRoleTemplate(entry.template as string), + }; + }), + } as RoleMapping; + }); + + if (expectSingleEntity) { + return response.ok({ body: mappings[0] }); + } + return response.ok({ body: mappings }); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); + + /** + * While role templates are normally persisted as objects via the create API, they are stored internally as strings. + * As a result, the ES APIs to retrieve role mappings represent the templates as strings, so we have to attempt + * to parse them back out. ES allows for invalid JSON to be stored, so we have to account for that as well. + * + * @param roleTemplate the string-based template to parse + */ + function tryParseRoleTemplate(roleTemplate: string) { + try { + return JSON.parse(roleTemplate); + } catch (e) { + logger.debug(`Role template is not valid JSON: ${e}`); + return roleTemplate; + } + } +} diff --git a/x-pack/plugins/security/server/routes/role_mapping/index.ts b/x-pack/plugins/security/server/routes/role_mapping/index.ts new file mode 100644 index 0000000000000..1bd90e8c1fae3 --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { defineRoleMappingFeatureCheckRoute } from './feature_check'; +import { defineRoleMappingGetRoutes } from './get'; +import { defineRoleMappingPostRoutes } from './post'; +import { defineRoleMappingDeleteRoutes } from './delete'; +import { RouteDefinitionParams } from '..'; + +export function defineRoleMappingRoutes(params: RouteDefinitionParams) { + defineRoleMappingFeatureCheckRoute(params); + defineRoleMappingGetRoutes(params); + defineRoleMappingPostRoutes(params); + defineRoleMappingDeleteRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts new file mode 100644 index 0000000000000..7d820d668a6da --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { routeDefinitionParamsMock } from '../index.mock'; +import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { defineRoleMappingPostRoutes } from './post'; + +describe('POST role mappings', () => { + it('allows a role mapping to be created', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ created: true }); + + defineRoleMappingPostRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.post.mock.calls; + + const name = 'mapping1'; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'post', + path: `/internal/security/role_mapping/${name}`, + params: { name }, + body: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, + headers, + }); + const mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ state: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(response.payload).toEqual({ created: true }); + + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.saveRoleMapping', + { + name, + body: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, + }, + }, + } + ); + }); + + describe('failure', () => { + it('returns result of license check', async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + defineRoleMappingPostRoutes(mockRouteDefinitionParams); + + const [[, handler]] = mockRouteDefinitionParams.router.post.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'post', + path: `/internal/security/role_mapping`, + headers, + }); + const mockContext = ({ + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: LICENSE_CHECK_STATE.Invalid, + message: 'test forbidden message', + }), + }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(403); + expect(response.payload).toEqual({ message: 'test forbidden message' }); + + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts new file mode 100644 index 0000000000000..bf9112be4ad3f --- /dev/null +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { wrapError } from '../../errors'; +import { RouteDefinitionParams } from '..'; + +export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { + const { clusterClient, router } = params; + + router.post( + { + path: '/internal/security/role_mapping/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + roles: schema.arrayOf(schema.string(), { defaultValue: [] }), + role_templates: schema.arrayOf( + schema.object({ + // Not validating `template` because the ES API currently accepts invalid payloads here. + // We allow this as well so that existing mappings can be updated via our Role Management UI + template: schema.any(), + format: schema.maybe( + schema.oneOf([schema.literal('string'), schema.literal('json')]) + ), + }), + { defaultValue: [] } + ), + enabled: schema.boolean(), + // Also lax on validation here because the real rules get quite complex, + // and keeping this in sync (and testable!) with ES could prove problematic. + // We do not interpret any of these rules within this route handler; + // they are simply passed to ES for processing. + rules: schema.object({}, { allowUnknowns: true }), + metadata: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const saveResponse = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.saveRoleMapping', { + name: request.params.name, + body: request.body, + }); + return response.ok({ body: saveResponse }); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.ts b/x-pack/plugins/spaces/server/lib/create_default_space.ts index 2301fa26dab28..0d1a4ddab91bb 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.ts @@ -9,7 +9,7 @@ import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server'; import { DEFAULT_SPACE_ID } from '../../common/constants'; interface Deps { - esClient: Pick; + esClient: IClusterClient; savedObjects: SavedObjectsLegacyService; } diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 2b0cfd3687a24..c1f557f164ad6 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -168,7 +168,7 @@ describe('onPostAuthInterceptor', () => { const spacesService = await service.setup({ http: (http as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index 9e1bb5cf9f7d6..a3396e98c3512 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -50,7 +50,7 @@ describe('createSpacesTutorialContextFactory', () => { it('should create context with the current space id for the default space', async () => { const spacesService = await service.setup({ http: coreMock.createSetup().http, - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index dc4bc839c0e29..6b7699100032d 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -5,7 +5,6 @@ */ import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; import { @@ -168,9 +167,8 @@ export class Plugin { ); }, createDefaultSpace: async () => { - const esClient = await core.elasticsearch.adminClient$.pipe(take(1)).toPromise(); - return createDefaultSpace({ - esClient, + return await createDefaultSpace({ + esClient: core.elasticsearch.adminClient, savedObjects: this.getLegacyAPI().savedObjects, }); }, diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 4d8d08a487e9a..74197e6ca7556 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -43,7 +43,7 @@ describe('copy to space', () => { const service = new SpacesService(log, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 28d5708a3873c..35f18cf66a57e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -44,7 +44,7 @@ describe('Spaces Public API', () => { const service = new SpacesService(log, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index f9bd4494791f1..3300e30825283 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -42,7 +42,7 @@ describe('GET space', () => { const service = new SpacesService(log, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index 02219db88a04c..ca89731f35946 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -42,7 +42,7 @@ describe('GET /spaces/space', () => { const service = new SpacesService(log, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index d82ccaa8ff380..26ecbf2247e0f 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -43,7 +43,7 @@ describe('Spaces Public API', () => { const service = new SpacesService(log, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index 15837110f4d92..e6182e027b854 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -44,7 +44,7 @@ describe('PUT /api/spaces/space', () => { const service = new SpacesService(log, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts index 8f97df2aa0a50..461f816ff5019 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts @@ -22,7 +22,7 @@ describe('GET /internal/spaces/_active_space', () => { const service = new SpacesService(null as any, () => legacyAPI); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), authorization: null, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index 30ad3f399916b..68d096e046ed4 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -74,7 +74,7 @@ const createService = async (serverBasePath: string = '') => { const spacesServiceSetup = await spacesService.setup({ http: httpSetup, - elasticsearch: elasticsearchServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetup(), config$: Rx.of(spacesConfig), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => new SpacesAuditLogger({}), diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index b8d0f910a42ea..f8ed58fa57551 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -5,7 +5,7 @@ */ import { map, take } from 'rxjs/operators'; -import { Observable, Subscription, combineLatest } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; @@ -69,15 +69,15 @@ export class SpacesService { }; const getScopedClient = async (request: KibanaRequest) => { - return combineLatest(elasticsearch.adminClient$, config$) + return config$ .pipe( - map(([clusterClient, config]) => { + map(config => { const internalRepository = this.getLegacyAPI().savedObjects.getSavedObjectsRepository( - clusterClient.callAsInternalUser, + elasticsearch.adminClient.callAsInternalUser, ['space'] ); - const callCluster = clusterClient.asScoped(request).callAsCurrentUser; + const callCluster = elasticsearch.adminClient.asScoped(request).callAsCurrentUser; const callWithRequestRepository = this.getLegacyAPI().savedObjects.getSavedObjectsRepository( callCluster, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index af43110a8ba5e..3b0c188318309 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -441,7 +441,6 @@ "common.ui.flotCharts.tueLabel": "火", "common.ui.flotCharts.wedLabel": "水", "common.ui.management.breadcrumb": "管理", - "common.ui.management.nav.menu": "管理メニュー", "common.ui.modals.cancelButtonLabel": "キャンセル", "common.ui.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", "common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストが接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", @@ -519,6 +518,7 @@ "common.ui.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "management.connectDataDisplayName": "データに接続", "management.displayName": "管理", + "management.nav.menu": "管理メニュー", "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", @@ -970,8 +970,8 @@ "kibana-react.savedObjects.saveModal.saveButtonLabel": "保存", "kibana-react.savedObjects.saveModal.saveTitle": "{objectType} を保存", "kibana-react.savedObjects.saveModal.titleLabel": "タイトル", - "kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", - "kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", + "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", + "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "inspector.closeButton": "インスペクターを閉じる", "inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です", "inspector.reqTimestampKey": "リクエストのタイムスタンプ", @@ -1850,6 +1850,25 @@ "kbn.discover.discoverDescription": "ドキュメントにクエリをかけたりフィルターを適用することで、データをインタラクティブに閲覧できます。", "kbn.discover.discoverTitle": "ディスカバー", "kbn.discover.documentsAriaLabel": "ドキュメント", + "kbn.discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip": "{underscoreSign} で始まるフィールド名はサポートされていません", + "kbn.discover.docViews.table.filterForFieldPresentButtonAriaLabel": "現在のフィールドのフィルター", + "kbn.discover.docViews.table.filterForFieldPresentButtonTooltip": "現在のフィールドのフィルター", + "kbn.discover.docViews.table.filterForValueButtonAriaLabel": "値でフィルタリング", + "kbn.discover.docViews.table.filterForValueButtonTooltip": "値でフィルタリング", + "kbn.discover.docViews.table.filterOutValueButtonAriaLabel": "値を除外", + "kbn.discover.docViews.table.filterOutValueButtonTooltip": "値を除外します", + "kbn.discover.docViews.table.noCachedMappingForThisFieldTooltip": "このフィールドのキャッシュされたマッピングがありません。管理 > インデックスパターンページからフィールドリストを更新してください", + "kbn.discover.docViews.table.tableTitle": "表", + "kbn.discover.docViews.table.toggleColumnInTableButtonAriaLabel": "表の列を切り替える", + "kbn.discover.docViews.table.toggleColumnInTableButtonTooltip": "表の列を切り替える", + "kbn.discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "メタフィールドの有無でフィルタリングできません", + "kbn.discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "スクリプトフィールドの有無でフィルタリングできません", + "kbn.discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "インデックスされていないフィールドは検索できません", + "kbn.discover.docViews.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", + "kbn.discover.docViews.json.jsonTitle": "JSON", + "kbn.discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", + "kbn.discover.docViews.table.noCachedMappingForThisFieldAriaLabel": "警告", + "kbn.discover.docViews.table.toggleFieldDetails": "フィールド詳細を切り替える", "kbn.discover.errorLoadingData": "データの読み込み中にエラーが発生", "kbn.discover.fetchError.howToAddressErrorDescription": "このエラーは、{scriptedFields} タブにある {managementLink} の {fetchErrorScript} フィールドを編集することで解決できます。", "kbn.discover.fetchError.managmentLinkText": "管理 > インデックスパターン", @@ -2834,25 +2853,6 @@ "kbn.advancedSettings.courier.batchSearchesText": "無効の場合、ダッシュボードパネルは個々に読み込まれ、検索リクエストはユーザーが移動するか\n クエリを更新すると停止します。有効の場合、ダッシュボードパネルはすべてのデータが読み込まれると同時に読み込まれ、\n 検索は停止しません。", "kbn.doc.couldNotFindDocumentsDescription": "その ID に一致するドキュメントがありません。", "kbn.doc.somethingWentWrongDescription": "{indexName} が欠けています。", - "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip": "{underscoreSign} で始まるフィールド名はサポートされていません", - "kbnDocViews.table.filterForFieldPresentButtonAriaLabel": "現在のフィールドのフィルター", - "kbnDocViews.table.filterForFieldPresentButtonTooltip": "現在のフィールドのフィルター", - "kbnDocViews.table.filterForValueButtonAriaLabel": "値でフィルタリング", - "kbnDocViews.table.filterForValueButtonTooltip": "値でフィルタリング", - "kbnDocViews.table.filterOutValueButtonAriaLabel": "値を除外", - "kbnDocViews.table.filterOutValueButtonTooltip": "値を除外します", - "kbnDocViews.table.noCachedMappingForThisFieldTooltip": "このフィールドのキャッシュされたマッピングがありません。管理 > インデックスパターンページからフィールドリストを更新してください", - "kbnDocViews.table.tableTitle": "表", - "kbnDocViews.table.toggleColumnInTableButtonAriaLabel": "表の列を切り替える", - "kbnDocViews.table.toggleColumnInTableButtonTooltip": "表の列を切り替える", - "kbnDocViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "メタフィールドの有無でフィルタリングできません", - "kbnDocViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "スクリプトフィールドの有無でフィルタリングできません", - "kbnDocViews.table.unindexedFieldsCanNotBeSearchedTooltip": "インデックスされていないフィールドは検索できません", - "kbnDocViews.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", - "kbnDocViews.json.jsonTitle": "JSON", - "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", - "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", - "kbnDocViews.table.toggleFieldDetails": "フィールド詳細を切り替える", "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", "kbnVislibVisTypes.area.areaTitle": "エリア", "kbnVislibVisTypes.area.groupTitle": "系列を分割", @@ -8048,7 +8048,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel": "異常スコア", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel": "下の境界", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel": "上の境界", - "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.valueLabel": "値", "xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel": "{numberOfCauses} 個の {plusSign}異常な{byFieldName}値", "xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel": "複数バケットの影響", "xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel": "予定イベント {counter}", @@ -10966,7 +10965,6 @@ "xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel": "サーバー側エコシステム", "xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel": "ストレージクラス", "xpack.snapshotRestore.repositoryDetails.typeTitle": "レポジトリタイプ", - "xpack.snapshotRestore.repositoryDetails.verificationDetails": "認証情報レポジトリ「{name}」", "xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle": "詳細", "xpack.snapshotRestore.repositoryDetails.verificationTitle": "認証ステータス", "xpack.snapshotRestore.repositoryDetails.verifyButtonLabel": "レポジトリを検証", @@ -11739,7 +11737,6 @@ "xpack.uptime.emptyState.loadingMessage": "読み込み中…", "xpack.uptime.emptyState.noDataTitle": "利用可能なアップタイムデータがありません", "xpack.uptime.emptyStateError.title": "エラー", - "xpack.uptime.emptyStatusBar.defaultMessage": "監視 ID {monitorId} のデータが見つかりません", "xpack.uptime.errorMessage": "エラー: {message}", "xpack.uptime.featureCatalogueDescription": "エンドポイントヘルスチェックとアップタイム監視を行います。", "xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム", @@ -11779,7 +11776,6 @@ "xpack.uptime.monitorList.statusColumn.upLabel": "アップ", "xpack.uptime.monitorList.statusColumnLabel": "ステータス", "xpack.uptime.monitorStatusBar.durationTextAriaLabel": "ミリ秒単位の監視時間", - "xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage": "{duration}ms", "xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "ダウン", "xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "アップ", "xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "監視ステータス", @@ -11816,9 +11812,9 @@ "xpack.uptime.pingList.expandRow": "拡張", "xpack.uptime.snapshot.pingsOverTimeTitle": "一定時間のピング", "xpack.uptime.snapshotHistogram.yAxis.title": "ピング", - "xpack.uptime.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total} 個中 {down} 個のモニターがダウンしています。", - "xpack.uptime.donutChart.legend.downRowLabel": "ダウン", - "xpack.uptime.donutChart.legend.upRowLabel": "アップ", + "xpack.uptime.snapshot.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total} 個中 {down} 個のモニターがダウンしています。", + "xpack.uptime.snapshot.donutChart.legend.downRowLabel": "ダウン", + "xpack.uptime.snapshot.donutChart.legend.upRowLabel": "アップ", "xpack.uptime.durationChart.emptyPrompt.description": "このモニターは選択された時間範囲で一度も {emphasizedText} していません。", "xpack.uptime.durationChart.emptyPrompt.title": "利用可能な期間データがありません", "xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0306edcabd67d..3cc476937d4e7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -441,7 +441,6 @@ "common.ui.flotCharts.tueLabel": "周二", "common.ui.flotCharts.wedLabel": "周三", "common.ui.management.breadcrumb": "管理", - "common.ui.management.nav.menu": "管理菜单", "common.ui.modals.cancelButtonLabel": "取消", "common.ui.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", "common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", @@ -520,6 +519,7 @@ "common.ui.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", "management.connectDataDisplayName": "连接数据", "management.displayName": "管理", + "management.nav.menu": "管理菜单", "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", @@ -971,8 +971,8 @@ "kibana-react.savedObjects.saveModal.saveButtonLabel": "保存", "kibana-react.savedObjects.saveModal.saveTitle": "保存 {objectType}", "kibana-react.savedObjects.saveModal.titleLabel": "标题", - "kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", - "kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", + "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", + "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", "inspector.closeButton": "关闭检查器", "inspector.reqTimestampDescription": "记录请求启动的时间", "inspector.reqTimestampKey": "请求时间戳", @@ -1851,6 +1851,25 @@ "kbn.discover.discoverDescription": "通过查询和筛选原始文档来以交互方式浏览您的数据。", "kbn.discover.discoverTitle": "Discover", "kbn.discover.documentsAriaLabel": "文档", + "kbn.discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip": "不支持以 {underscoreSign} 开头的字段名称", + "kbn.discover.docViews.table.filterForFieldPresentButtonAriaLabel": "筛留存在的字段", + "kbn.discover.docViews.table.filterForFieldPresentButtonTooltip": "筛留存在的字段", + "kbn.discover.docViews.table.filterForValueButtonAriaLabel": "筛留值", + "kbn.discover.docViews.table.filterForValueButtonTooltip": "筛留值", + "kbn.discover.docViews.table.filterOutValueButtonAriaLabel": "筛除值", + "kbn.discover.docViews.table.filterOutValueButtonTooltip": "筛除值", + "kbn.discover.docViews.table.noCachedMappingForThisFieldTooltip": "此字段没有任何已缓存映射。从“管理”>“索引模式”页面刷新字段列表", + "kbn.discover.docViews.table.tableTitle": "表", + "kbn.discover.docViews.table.toggleColumnInTableButtonAriaLabel": "在表中切换列", + "kbn.discover.docViews.table.toggleColumnInTableButtonTooltip": "在表中切换列", + "kbn.discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "无法筛留元数据字段", + "kbn.discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "无法筛留脚本字段", + "kbn.discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "无法搜索未索引字段", + "kbn.discover.docViews.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", + "kbn.discover.docViews.json.jsonTitle": "JSON", + "kbn.discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", + "kbn.discover.docViews.table.noCachedMappingForThisFieldAriaLabel": "警告", + "kbn.discover.docViews.table.toggleFieldDetails": "切换字段详细信息", "kbn.discover.errorLoadingData": "加载数据时出错", "kbn.discover.fetchError.howToAddressErrorDescription": "您可以通过编辑 “{scriptedFields}” 选项卡下 “{managementLink}” 中的 “{fetchErrorScript}” 字段来解决此错误。", "kbn.discover.fetchError.managmentLinkText": "管理 > 索引模式", @@ -2835,25 +2854,6 @@ "kbn.advancedSettings.courier.batchSearchesText": "禁用时,仪表板面板将分别加载,用户离开时或更新查询时,\n 搜索请求将终止。启用时,仪表板面板将一起加载并加载所有数据,\n 搜索将不会终止。", "kbn.doc.couldNotFindDocumentsDescription": "无文档匹配该 ID。", "kbn.doc.somethingWentWrongDescription": "{indexName} 缺失。", - "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip": "不支持以 {underscoreSign} 开头的字段名称", - "kbnDocViews.table.filterForFieldPresentButtonAriaLabel": "筛留存在的字段", - "kbnDocViews.table.filterForFieldPresentButtonTooltip": "筛留存在的字段", - "kbnDocViews.table.filterForValueButtonAriaLabel": "筛留值", - "kbnDocViews.table.filterForValueButtonTooltip": "筛留值", - "kbnDocViews.table.filterOutValueButtonAriaLabel": "筛除值", - "kbnDocViews.table.filterOutValueButtonTooltip": "筛除值", - "kbnDocViews.table.noCachedMappingForThisFieldTooltip": "此字段没有任何已缓存映射。从“管理”>“索引模式”页面刷新字段列表", - "kbnDocViews.table.tableTitle": "表", - "kbnDocViews.table.toggleColumnInTableButtonAriaLabel": "在表中切换列", - "kbnDocViews.table.toggleColumnInTableButtonTooltip": "在表中切换列", - "kbnDocViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "无法筛留元数据字段", - "kbnDocViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "无法筛留脚本字段", - "kbnDocViews.table.unindexedFieldsCanNotBeSearchedTooltip": "无法搜索未索引字段", - "kbnDocViews.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", - "kbnDocViews.json.jsonTitle": "JSON", - "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", - "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", - "kbnDocViews.table.toggleFieldDetails": "切换字段详细信息", "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", "kbnVislibVisTypes.area.areaTitle": "面积图", "kbnVislibVisTypes.area.groupTitle": "拆分序列", @@ -8137,7 +8137,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel": "异常分数", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel": "下边界", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel": "上边界", - "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.valueLabel": "值", "xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel": "{numberOfCauses}{plusSign} 异常 {byFieldName} 值", "xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel": "多存储桶影响", "xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel": "已计划事件{counter}", @@ -11055,7 +11054,6 @@ "xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel": "服务器端加密", "xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel": "存储类", "xpack.snapshotRestore.repositoryDetails.typeTitle": "存储库类型", - "xpack.snapshotRestore.repositoryDetails.verificationDetails": "验证详情存储库“{name}”", "xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle": "详情", "xpack.snapshotRestore.repositoryDetails.verificationTitle": "验证状态", "xpack.snapshotRestore.repositoryDetails.verifyButtonLabel": "验证存储库", @@ -11828,7 +11826,6 @@ "xpack.uptime.emptyState.loadingMessage": "正在加载……", "xpack.uptime.emptyState.noDataTitle": "没有可用的运行时间数据", "xpack.uptime.emptyStateError.title": "错误", - "xpack.uptime.emptyStatusBar.defaultMessage": "未找到监测 ID {monitorId} 的数据", "xpack.uptime.errorMessage": "错误:{message}", "xpack.uptime.featureCatalogueDescription": "执行终端节点运行状况检查和运行时间监测。", "xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间", @@ -11868,7 +11865,6 @@ "xpack.uptime.monitorList.statusColumn.upLabel": "运行", "xpack.uptime.monitorList.statusColumnLabel": "状态", "xpack.uptime.monitorStatusBar.durationTextAriaLabel": "监测持续时间(毫秒)", - "xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage": "{duration}ms", "xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "关闭", "xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "运行", "xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "检测状态", @@ -11905,9 +11901,9 @@ "xpack.uptime.pingList.expandRow": "展开", "xpack.uptime.snapshot.pingsOverTimeTitle": "时移 Ping 数", "xpack.uptime.snapshotHistogram.yAxis.title": "Ping", - "xpack.uptime.donutChart.ariaLabel": "显示当前状态的饼图。{down} 个监测已关闭,共 {total} 个。", - "xpack.uptime.donutChart.legend.downRowLabel": "关闭", - "xpack.uptime.donutChart.legend.upRowLabel": "运行", + "xpack.uptime.snapshot.donutChart.ariaLabel": "显示当前状态的饼图。{down} 个监测已关闭,共 {total} 个。", + "xpack.uptime.snapshot.donutChart.legend.downRowLabel": "关闭", + "xpack.uptime.snapshot.donutChart.legend.upRowLabel": "运行", "xpack.uptime.durationChart.emptyPrompt.description": "在选定时间范围内此监测从未{emphasizedText}。", "xpack.uptime.durationChart.emptyPrompt.title": "没有持续时间数据", "xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。", diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 2b92e70fb30af..bda5b51623d05 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -4,38 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -require('@kbn/plugin-helpers').babelRegister(); -require('@kbn/test').runTestsCli([ +const alwaysImportedTests = [require.resolve('../test/functional/config.js')]; +const onlyNotInCoverageTests = [ require.resolve('../test/reporting/configs/chromium_api.js'), require.resolve('../test/reporting/configs/chromium_functional.js'), - require.resolve('../test/reporting/configs/generate_api'), - require.resolve('../test/functional/config.js'), + require.resolve('../test/reporting/configs/generate_api.js'), + require.resolve('../test/functional/config_security_basic.js'), require.resolve('../test/api_integration/config_security_basic.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.js'), - require.resolve('../test/plugin_functional/config'), - require.resolve('../test/kerberos_api_integration/config'), - require.resolve('../test/kerberos_api_integration/anonymous_access.config'), - require.resolve('../test/saml_api_integration/config'), - require.resolve('../test/token_api_integration/config'), - require.resolve('../test/oidc_api_integration/config'), - require.resolve('../test/oidc_api_integration/implicit_flow.config'), - require.resolve('../test/pki_api_integration/config'), - require.resolve('../test/spaces_api_integration/spaces_only/config'), - require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial'), - require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic'), - require.resolve('../test/saved_object_api_integration/security_and_spaces/config_trial'), - require.resolve('../test/saved_object_api_integration/security_and_spaces/config_basic'), - require.resolve('../test/saved_object_api_integration/security_only/config_trial'), - require.resolve('../test/saved_object_api_integration/security_only/config_basic'), - require.resolve('../test/saved_object_api_integration/spaces_only/config'), - require.resolve('../test/ui_capabilities/security_and_spaces/config'), - require.resolve('../test/ui_capabilities/security_only/config'), - require.resolve('../test/ui_capabilities/spaces_only/config'), - require.resolve('../test/upgrade_assistant_integration/config'), - require.resolve('../test/licensing_plugin/config'), - require.resolve('../test/licensing_plugin/config.public'), - require.resolve('../test/licensing_plugin/config.legacy'), + require.resolve('../test/plugin_functional/config.ts'), + require.resolve('../test/kerberos_api_integration/config.ts'), + require.resolve('../test/kerberos_api_integration/anonymous_access.config.ts'), + require.resolve('../test/saml_api_integration/config.ts'), + require.resolve('../test/token_api_integration/config.js'), + require.resolve('../test/oidc_api_integration/config.ts'), + require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'), + require.resolve('../test/pki_api_integration/config.ts'), + require.resolve('../test/spaces_api_integration/spaces_only/config.ts'), + require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'), + require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'), + require.resolve('../test/saved_object_api_integration/security_and_spaces/config_trial.ts'), + require.resolve('../test/saved_object_api_integration/security_and_spaces/config_basic.ts'), + require.resolve('../test/saved_object_api_integration/security_only/config_trial.ts'), + require.resolve('../test/saved_object_api_integration/security_only/config_basic.ts'), + require.resolve('../test/saved_object_api_integration/spaces_only/config.ts'), + require.resolve('../test/ui_capabilities/security_and_spaces/config.ts'), + require.resolve('../test/ui_capabilities/security_only/config.ts'), + require.resolve('../test/ui_capabilities/spaces_only/config.ts'), + require.resolve('../test/upgrade_assistant_integration/config.js'), + require.resolve('../test/licensing_plugin/config.ts'), + require.resolve('../test/licensing_plugin/config.public.ts'), + require.resolve('../test/licensing_plugin/config.legacy.ts'), +]; + +require('@kbn/plugin-helpers').babelRegister(); +require('@kbn/test').runTestsCli([ + ...alwaysImportedTests, + ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests), ]); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index ebe741df71d79..b5d201c1682bd 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -202,8 +202,21 @@ export default function(kibana: any) { id: 'test.always-firing', name: 'Test: Always Firing', actionGroups: ['default', 'other'], - async executor({ services, params, state }: AlertExecutorOptions) { + async executor(alertExecutorOptions: AlertExecutorOptions) { + const { + services, + params, + state, + alertId, + spaceId, + namespace, + name, + tags, + createdBy, + updatedBy, + } = alertExecutorOptions; let group = 'default'; + const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy }; if (params.groupsToScheduleActionsInSeries) { const index = state.groupInSeriesIndex || 0; @@ -226,6 +239,7 @@ export default function(kibana: any) { params, reference: params.reference, source: 'alert:test.always-firing', + alertInfo, }, }); return { diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index c47649544f9a7..c793af359489a 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -24,6 +24,14 @@ export interface CreateAlertWithActionOpts { reference: string; } +interface UpdateAlwaysFiringAction { + alertId: string; + actionId: string | undefined; + reference: string; + user: User; + overwrites: Record; +} + export class AlertUtils { private referenceCounter = 1; private readonly user?: User; @@ -176,38 +184,41 @@ export class AlertUtils { if (this.user) { request = request.auth(this.user.username, this.user.password); } - const response = await request.send({ - enabled: true, - name: 'abc', - schedule: { interval: '1m' }, - throttle: '1m', - tags: [], - alertTypeId: 'test.always-firing', - consumer: 'bar', - params: { - index: ES_TEST_INDEX_NAME, - reference, - }, - actions: [ - { - group: 'default', - id: this.indexRecordActionId, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: - 'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}', - }, - }, - ], - ...overwrites, - }); + const alertBody = getDefaultAlwaysFiringAlertData(reference, actionId); + const response = await request.send({ ...alertBody, ...overwrites }); if (response.statusCode === 200) { objRemover.add(this.space.id, response.body.id, 'alert'); } return response; } + public async updateAlwaysFiringAction({ + alertId, + actionId, + reference, + user, + overwrites = {}, + }: UpdateAlwaysFiringAction) { + actionId = actionId || this.indexRecordActionId; + + if (!actionId) { + throw new Error('actionId is required '); + } + + const request = this.supertestWithoutAuth + .put(`${getUrlPrefix(this.space.id)}/api/alert/${alertId}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + const alertBody = getDefaultAlwaysFiringAlertData(reference, actionId); + delete alertBody.alertTypeId; + delete alertBody.enabled; + delete alertBody.consumer; + + const response = await request.send({ ...alertBody, ...overwrites }); + return response; + } + public async createAlwaysFailingAction({ objectRemover, overwrites = {}, @@ -251,3 +262,31 @@ export class AlertUtils { return response; } } + +function getDefaultAlwaysFiringAlertData(reference: string, actionId: string) { + return { + enabled: true, + name: 'abc', + schedule: { interval: '1m' }, + throttle: '1m', + tags: [], + alertTypeId: 'test.always-firing', + consumer: 'bar', + params: { + index: ES_TEST_INDEX_NAME, + reference, + }, + actions: [ + { + group: 'default', + id: actionId, + params: { + index: ES_TEST_INDEX_NAME, + reference, + message: + 'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}', + }, + }, + ], + }; +} diff --git a/x-pack/test/alerting_api_integration/common/types.ts b/x-pack/test/alerting_api_integration/common/types.ts index e94add5bbcd28..c4a341435aaaa 100644 --- a/x-pack/test/alerting_api_integration/common/types.ts +++ b/x-pack/test/alerting_api_integration/common/types.ts @@ -52,6 +52,7 @@ export interface User { export interface Space { id: string; + namespace?: string; name: string; disabledFeatures: string[]; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index 354d87bd11bb2..d58fcd29e29fc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -29,7 +29,7 @@ const NoKibanaPrivileges: User = { }, }; -const Superuser: User = { +export const Superuser: User = { username: 'superuser', fullName: 'superuser', password: 'superuser-password', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 69bc547e3bfc1..551498e22d5c8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ESTestIndexTool, @@ -96,7 +96,9 @@ export default function alertTests({ getService }: FtrProviderContext) { // Wait for the action to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference); - await alertUtils.disable(response.body.id); + + const alertId = response.body.id; + await alertUtils.disable(alertId); await taskManagerUtils.waitForIdle(testStart); // Ensure only 1 alert executed with proper params @@ -113,6 +115,15 @@ export default function alertTests({ getService }: FtrProviderContext) { index: ES_TEST_INDEX_NAME, reference, }, + alertInfo: { + alertId, + spaceId: space.id, + namespace: space.id, + name: 'abc', + tags: [], + createdBy: user.fullName, + updatedBy: user.fullName, + }, }); // Ensure only 1 action executed with proper params @@ -142,6 +153,56 @@ export default function alertTests({ getService }: FtrProviderContext) { } }); + it('should pass updated alert params to executor', async () => { + // create an alert + const reference = alertUtils.generateReference(); + const overwrites = { + throttle: '1s', + schedule: { interval: '1s' }, + }; + const response = await alertUtils.createAlwaysFiringAction({ reference, overwrites }); + + // only need to test creation success paths + if (response.statusCode !== 200) return; + + // update the alert with super user + const alertId = response.body.id; + const reference2 = alertUtils.generateReference(); + const response2 = await alertUtils.updateAlwaysFiringAction({ + alertId, + actionId: indexRecordActionId, + user: Superuser, + reference: reference2, + overwrites: { + name: 'def', + tags: ['fee', 'fi', 'fo'], + throttle: '1s', + schedule: { interval: '1s' }, + }, + }); + + expect(response2.statusCode).to.eql(200); + + // make sure alert info passed to executor is correct + await esTestIndexTool.waitForDocs('alert:test.always-firing', reference2); + await alertUtils.disable(alertId); + const alertSearchResult = await esTestIndexTool.search( + 'alert:test.always-firing', + reference2 + ); + + expect(alertSearchResult.hits.total.value).to.be.greaterThan(0); + expect(alertSearchResult.hits.hits[0]._source.alertInfo).to.eql({ + alertId, + spaceId: space.id, + namespace: space.id, + name: 'def', + tags: ['fee', 'fi', 'fo'], + createdBy: user.fullName, + updatedBy: Superuser.fullName, + }); + }); + it('should handle custom retry logic when appropriate', async () => { const testStart = new Date(); // We have to provide the test.rate-limit the next runAt, for testing purposes diff --git a/x-pack/test/alerting_api_integration/spaces_only/scenarios.ts b/x-pack/test/alerting_api_integration/spaces_only/scenarios.ts index 2b91c408d3de9..c2b3ec6148036 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/scenarios.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/scenarios.ts @@ -8,17 +8,27 @@ import { Space } from '../common/types'; const Space1: Space = { id: 'space1', + namespace: 'space1', name: 'Space 1', disabledFeatures: [], }; const Other: Space = { id: 'other', + namespace: 'other', name: 'Other', disabledFeatures: [], }; +const Default: Space = { + id: 'default', + namespace: undefined, + name: 'Default', + disabledFeatures: [], +}; + export const Spaces = { space1: Space1, other: Other, + default: Default, }; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts similarity index 88% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 032fee15882cf..d9a58851afb31 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { Spaces } from '../../scenarios'; +import { Space } from '../../../common/types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ESTestIndexTool, @@ -19,7 +19,7 @@ import { } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export -export default function alertTests({ getService }: FtrProviderContext) { +export function alertTests({ getService }: FtrProviderContext, space: Space) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('legacyEs'); const retry = getService('retry'); @@ -43,7 +43,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexTool.setup(); await es.indices.create({ index: authorizationIndex }); const { body: createdAction } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', @@ -58,7 +58,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .expect(200); indexRecordActionId = createdAction.id; alertUtils = new AlertUtils({ - space: Spaces.space1, + space, supertestWithoutAuth, indexRecordActionId, objectRemover, @@ -68,19 +68,20 @@ export default function alertTests({ getService }: FtrProviderContext) { after(async () => { await esTestIndexTool.destroy(); await es.indices.delete({ index: authorizationIndex }); - objectRemover.add(Spaces.space1.id, indexRecordActionId, 'action'); + objectRemover.add(space.id, indexRecordActionId, 'action'); await objectRemover.removeAll(); }); it('should schedule task, run alert and schedule actions', async () => { const reference = alertUtils.generateReference(); const response = await alertUtils.createAlwaysFiringAction({ reference }); + const alertId = response.body.id; expect(response.statusCode).to.eql(200); const alertTestRecord = ( await esTestIndexTool.waitForDocs('alert:test.always-firing', reference) )[0]; - expect(alertTestRecord._source).to.eql({ + const expected = { source: 'alert:test.always-firing', reference, state: {}, @@ -88,7 +89,20 @@ export default function alertTests({ getService }: FtrProviderContext) { index: ES_TEST_INDEX_NAME, reference, }, - }); + alertInfo: { + alertId, + spaceId: space.id, + namespace: space.namespace, + name: 'abc', + tags: [], + createdBy: null, + updatedBy: null, + }, + }; + if (expected.alertInfo.namespace === undefined) { + delete expected.alertInfo.namespace; + } + expect(alertTestRecord._source).to.eql(expected); const actionTestRecord = ( await esTestIndexTool.waitForDocs('action:test.index-record', reference) )[0]; @@ -147,7 +161,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const retryDate = new Date(Date.now() + 60000); const { body: createdAction } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') .send({ name: 'Test rate limit', @@ -155,11 +169,11 @@ export default function alertTests({ getService }: FtrProviderContext) { config: {}, }) .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); + objectRemover.add(space.id, createdAction.id, 'action'); const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -184,7 +198,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ); expect(response.statusCode).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); + objectRemover.add(space.id, response.body.id, 'alert'); const scheduledActionTask = await retry.try(async () => { const searchResult = await es.search({ index: '.kibana_task_manager', @@ -228,7 +242,7 @@ export default function alertTests({ getService }: FtrProviderContext) { it('should have proper callCluster and savedObjectsClient authorization for alert type executor', async () => { const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -244,7 +258,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ); expect(response.statusCode).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); + objectRemover.add(space.id, response.body.id, 'alert'); const alertTestRecord = ( await esTestIndexTool.waitForDocs('alert:test.authorization', reference) )[0]; @@ -264,16 +278,16 @@ export default function alertTests({ getService }: FtrProviderContext) { it('should have proper callCluster and savedObjectsClient authorization for action type executor', async () => { const reference = alertUtils.generateReference(); const { body: createdAction } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', actionTypeId: 'test.authorization', }) .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); + objectRemover.add(space.id, createdAction.id, 'action'); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -299,7 +313,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ); expect(response.statusCode).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); + objectRemover.add(space.id, response.body.id, 'alert'); const actionTestRecord = ( await esTestIndexTool.waitForDocs('action:test.authorization', reference) )[0]; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_default_space.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_default_space.ts new file mode 100644 index 0000000000000..3e677952d8700 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_default_space.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 '../../../common/ftr_provider_context'; +import { Spaces } from '../../scenarios'; +import { alertTests } from './alerts_base'; + +// eslint-disable-next-line import/no-default-export +export default function alertSpace1Tests(context: FtrProviderContext) { + alertTests(context, Spaces.default); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_space1.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_space1.ts new file mode 100644 index 0000000000000..07ad4cd294ab3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_space1.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 '../../../common/ftr_provider_context'; +import { Spaces } from '../../scenarios'; +import { alertTests } from './alerts_base'; + +// eslint-disable-next-line import/no-default-export +export default function alertSpace1Tests(context: FtrProviderContext) { + alertTests(context, Spaces.space1); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 1aa084356cfa4..569c0d538d473 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -22,6 +22,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./unmute_instance')); loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); - loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./alerts_space1')); + loadTestFile(require.resolve('./alerts_default_space')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts index f18aebaf4e689..cb2b17980d37a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts @@ -20,7 +20,10 @@ export default function alertingApiIntegrationTests({ before(async () => { for (const space of Object.values(Spaces)) { - await spacesService.create(space); + if (space.id === 'default') continue; + + const { id, name, disabledFeatures } = space; + await spacesService.create({ id, name, disabledFeatures }); } }); diff --git a/x-pack/test/api_integration/apis/endpoint/endpoints.ts b/x-pack/test/api_integration/apis/endpoint/endpoints.ts new file mode 100644 index 0000000000000..32864489d3786 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/endpoints.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/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + describe('test endpoints api', () => { + describe('POST /api/endpoint/endpoints when index is empty', () => { + it('endpoints api should return empty result when index is empty', async () => { + await esArchiver.unload('endpoint/endpoints'); + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + expect(body.total).to.eql(0); + expect(body.endpoints.length).to.eql(0); + expect(body.request_page_size).to.eql(10); + expect(body.request_index).to.eql(0); + }); + }); + + describe('POST /api/endpoint/endpoints when index is not empty', () => { + before(() => esArchiver.load('endpoint/endpoints')); + after(() => esArchiver.unload('endpoint/endpoints')); + it('endpoints api should return one entry for each endpoint with default paging', async () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).to.eql(3); + expect(body.request_page_size).to.eql(10); + expect(body.request_index).to.eql(0); + }); + + it('endpoints api should return page based on params passed.', async () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send({ + paging_properties: [ + { + page_size: 1, + }, + { + page_index: 1, + }, + ], + }) + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).to.eql(1); + expect(body.request_page_size).to.eql(1); + expect(body.request_index).to.eql(1); + }); + + /* test that when paging properties produces no result, the total should reflect the actual number of endpoints + in the index. + */ + it('endpoints api should return accurate total endpoints if page index produces no result', async () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send({ + paging_properties: [ + { + page_size: 10, + }, + { + page_index: 3, + }, + ], + }) + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).to.eql(0); + expect(body.request_page_size).to.eql(10); + expect(body.request_index).to.eql(30); + }); + + it('endpoints api should return 400 when pagingProperties is below boundaries.', async () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send({ + paging_properties: [ + { + page_size: 0, + }, + { + page_index: 1, + }, + ], + }) + .expect(400); + expect(body.message).to.contain('Value is [0] but it must be equal to or greater than [1]'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts index e0ffbb13e5978..a3f0e828d7240 100644 --- a/x-pack/test/api_integration/apis/endpoint/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Endpoint plugin', function() { loadTestFile(require.resolve('./resolver')); + loadTestFile(require.resolve('./endpoints')); }); } diff --git a/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts b/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts index 67529c77f70f7..66701de2704a6 100644 --- a/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts +++ b/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts @@ -110,7 +110,9 @@ export default function({ getService }: FtrProviderContext) { }); }); - it('highlights field columns', async () => { + // Skipped since it behaves differently in master and in the 7.X branch + // See https://github.com/elastic/kibana/issues/49959 + it.skip('highlights field columns', async () => { const { body } = await supertest .post(LOG_ENTRIES_HIGHLIGHTS_PATH) .set(COMMON_HEADERS) @@ -140,9 +142,7 @@ export default function({ getService }: FtrProviderContext) { entries.forEach(entry => { entry.columns.forEach(column => { if ('field' in column && 'highlights' in column && column.highlights.length > 0) { - // https://github.com/elastic/kibana/issues/49959 - // expect(column.highlights).to.eql(['generate_test_data/simple_logs']); - expect(column.highlights).to.eql(['generate_test_data']); + expect(column.highlights).to.eql(['generate_test_data/simple_logs']); } }); }); diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/index_detail.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/index_detail.json index 04d56d5949d2c..65094144d6ff0 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/index_detail.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/index_detail.json @@ -9,21 +9,6 @@ "totalShards": 10, "status": "green" }, - "logs": { - "enabled": false, - "limit": 10, - "reason": { - "clusterExists": false, - "indexPatternExists": false, - "indexPatternInTimeRangeExists": false, - "typeExistsAtAnyTime": false, - "usingStructuredLogs": false, - "nodeExists": null, - "indexExists": false, - "typeExists": false - }, - "logs": [] - }, "metrics": { "index_search_request_rate": [ { @@ -1104,93 +1089,108 @@ } ] }, + "logs": { + "enabled": false, + "logs": [], + "reason": { + "indexPatternExists": false, + "indexPatternInTimeRangeExists": false, + "typeExistsAtAnyTime": false, + "typeExists": false, + "usingStructuredLogs": false, + "clusterExists": false, + "nodeExists": null, + "indexExists": false + }, + "limit": 10 + }, "shards": [ { - "state": "STARTED", - "primary": false, + "index": "avocado-tweets-2017.10.02", "node": "xcP6ue7eRCieNNitFTT0EA", + "primary": false, "relocating_node": null, "shard": 4, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": true, + "index": "avocado-tweets-2017.10.02", "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, "relocating_node": null, "shard": 4, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": true, + "index": "avocado-tweets-2017.10.02", "node": "xcP6ue7eRCieNNitFTT0EA", + "primary": true, "relocating_node": null, "shard": 1, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": false, + "index": "avocado-tweets-2017.10.02", "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": false, "relocating_node": null, "shard": 1, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": true, + "index": "avocado-tweets-2017.10.02", "node": "xcP6ue7eRCieNNitFTT0EA", + "primary": true, "relocating_node": null, "shard": 2, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": false, + "index": "avocado-tweets-2017.10.02", "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": false, "relocating_node": null, "shard": 2, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": false, + "index": "avocado-tweets-2017.10.02", "node": "xcP6ue7eRCieNNitFTT0EA", + "primary": false, "relocating_node": null, "shard": 3, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": true, + "index": "avocado-tweets-2017.10.02", "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, "relocating_node": null, "shard": 3, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": false, + "index": "avocado-tweets-2017.10.02", "node": "xcP6ue7eRCieNNitFTT0EA", + "primary": false, "relocating_node": null, "shard": 0, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" }, { - "state": "STARTED", - "primary": true, + "index": "avocado-tweets-2017.10.02", "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, "relocating_node": null, "shard": 0, - "index": "avocado-tweets-2017.10.02" + "state": "STARTED" } ], "shardStats": { "nodes": { "jUT5KdxfRbORSCWkb5zjmA": { - "shardCount": 38, - "indexCount": 20, + "shardCount": 5, + "indexCount": 1, "name": "whatever-01", "node_ids": [ "jUT5KdxfRbORSCWkb5zjmA" @@ -1198,29 +1198,20 @@ "type": "master" }, "xcP6ue7eRCieNNitFTT0EA": { - "shardCount": 36, - "indexCount": 19, + "shardCount": 5, + "indexCount": 1, "name": "whatever-02", "node_ids": [ "xcP6ue7eRCieNNitFTT0EA" ], "type": "node" - }, - "bwQWH-7IQY-mFPpfoaoFXQ": { - "shardCount": 4, - "indexCount": 4, - "name": "whatever-03", - "node_ids": [ - "bwQWH-7IQY-mFPpfoaoFXQ" - ], - "type": "node" } } }, "nodes": { "jUT5KdxfRbORSCWkb5zjmA": { - "shardCount": 38, - "indexCount": 20, + "shardCount": 5, + "indexCount": 1, "name": "whatever-01", "node_ids": [ "jUT5KdxfRbORSCWkb5zjmA" @@ -1228,22 +1219,13 @@ "type": "master" }, "xcP6ue7eRCieNNitFTT0EA": { - "shardCount": 36, - "indexCount": 19, + "shardCount": 5, + "indexCount": 1, "name": "whatever-02", "node_ids": [ "xcP6ue7eRCieNNitFTT0EA" ], "type": "node" - }, - "bwQWH-7IQY-mFPpfoaoFXQ": { - "shardCount": 4, - "indexCount": 4, - "name": "whatever-03", - "node_ids": [ - "bwQWH-7IQY-mFPpfoaoFXQ" - ], - "type": "node" } }, "stateUuid": "6wwwErXyTfaa4uHBHG5Pbg" diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail.json index 0b8d26558e7fc..32096b0b97067 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail.json @@ -1,293 +1,1518 @@ { "nodeSummary": { - "resolver": "jxcP6ue7eRCieNNitFTT0EA", - "node_ids": [], - "attributes": {}, - "transport_address": "", - "name": "jxcP6ue7eRCieNNitFTT0EA", - "type": "node", - "nodeTypeLabel": "Offline Node", - "status": "Offline", - "isOnline": false + "resolver": "jUT5KdxfRbORSCWkb5zjmA", + "node_ids": [ + "jUT5KdxfRbORSCWkb5zjmA" + ], + "attributes": { + "ml.enabled": "true", + "ml.max_open_jobs": "10" + }, + "transport_address": "127.0.0.1:9300", + "name": "whatever-01", + "type": "master", + "nodeTypeLabel": "Master Node", + "nodeTypeClass": "starFilled", + "totalShards": 38, + "indexCount": 20, + "documents": 24830, + "dataSize": 52847579, + "freeSpace": 186755088384, + "totalSpace": 499065712640, + "usedHeap": 29, + "status": "Online", + "isOnline": true + }, + "metrics": { + "node_total_io": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.fs.io_stats.total.operations", + "metricAgg": "max", + "label": "Total I/O", + "title": "I/O Operations Rate", + "description": "Total I/O. (This metric is not supported on all platforms and may display N/A if I/O data is unavailable.)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1507235520000, + null + ], + [ + 1507235530000, + null + ], + [ + 1507235540000, + null + ], + [ + 1507235550000, + null + ], + [ + 1507235560000, + null + ], + [ + 1507235570000, + null + ], + [ + 1507235580000, + null + ], + [ + 1507235590000, + null + ], + [ + 1507235600000, + null + ], + [ + 1507235610000, + null + ], + [ + 1507235620000, + null + ], + [ + 1507235630000, + null + ], + [ + 1507235640000, + null + ], + [ + 1507235650000, + null + ], + [ + 1507235660000, + null + ], + [ + 1507235670000, + null + ], + [ + 1507235680000, + null + ], + [ + 1507235690000, + null + ], + [ + 1507235700000, + null + ] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.fs.io_stats.total.read_operations", + "metricAgg": "max", + "label": "Total Read I/O", + "title": "I/O Operations Rate", + "description": "Total Read I/O. (This metric is not supported on all platforms and may display N/A if I/O data is unavailable.)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1507235520000, + null + ], + [ + 1507235530000, + null + ], + [ + 1507235540000, + null + ], + [ + 1507235550000, + null + ], + [ + 1507235560000, + null + ], + [ + 1507235570000, + null + ], + [ + 1507235580000, + null + ], + [ + 1507235590000, + null + ], + [ + 1507235600000, + null + ], + [ + 1507235610000, + null + ], + [ + 1507235620000, + null + ], + [ + 1507235630000, + null + ], + [ + 1507235640000, + null + ], + [ + 1507235650000, + null + ], + [ + 1507235660000, + null + ], + [ + 1507235670000, + null + ], + [ + 1507235680000, + null + ], + [ + 1507235690000, + null + ], + [ + 1507235700000, + null + ] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.fs.io_stats.total.write_operations", + "metricAgg": "max", + "label": "Total Write I/O", + "title": "I/O Operations Rate", + "description": "Total Write I/O. (This metric is not supported on all platforms and may display N/A if I/O data is unavailable.)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1507235520000, + null + ], + [ + 1507235530000, + null + ], + [ + 1507235540000, + null + ], + [ + 1507235550000, + null + ], + [ + 1507235560000, + null + ], + [ + 1507235570000, + null + ], + [ + 1507235580000, + null + ], + [ + 1507235590000, + null + ], + [ + 1507235600000, + null + ], + [ + 1507235610000, + null + ], + [ + 1507235620000, + null + ], + [ + 1507235630000, + null + ], + [ + 1507235640000, + null + ], + [ + 1507235650000, + null + ], + [ + 1507235660000, + null + ], + [ + 1507235670000, + null + ], + [ + 1507235680000, + null + ], + [ + 1507235690000, + null + ], + [ + 1507235700000, + null + ] + ] + } + ], + "node_latency": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.search.query_total", + "metricAgg": "sum", + "label": "Search", + "title": "Latency", + "description": "Average latency for searching, which is time it takes to execute searches divided by number of searches submitted. This considers primary and replica shards.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": true, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + null + ], + [ + 1507235530000, + 0.33333333333333337 + ], + [ + 1507235540000, + 0 + ], + [ + 1507235550000, + 0.33333333333333337 + ], + [ + 1507235560000, + 0 + ], + [ + 1507235570000, + 0.33333333333333337 + ], + [ + 1507235580000, + 0 + ], + [ + 1507235590000, + 0 + ], + [ + 1507235600000, + 0.33333333333333337 + ], + [ + 1507235610000, + 0 + ], + [ + 1507235620000, + 0 + ], + [ + 1507235630000, + 0.33333333333333337 + ], + [ + 1507235640000, + 0 + ], + [ + 1507235650000, + 0 + ], + [ + 1507235660000, + 0 + ], + [ + 1507235670000, + 0.2 + ], + [ + 1507235680000, + 0 + ], + [ + 1507235690000, + 0 + ], + [ + 1507235700000, + 0 + ] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.indexing.index_total", + "metricAgg": "sum", + "label": "Indexing", + "title": "Latency", + "description": "Average latency for indexing documents, which is time it takes to index documents divided by number that were indexed. This considers any shard located on this node, including replicas.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": true, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + null + ], + [ + 1507235530000, + 0 + ], + [ + 1507235540000, + 0 + ], + [ + 1507235550000, + 0.888888888888889 + ], + [ + 1507235560000, + 1.1666666666666667 + ], + [ + 1507235570000, + 0 + ], + [ + 1507235580000, + 0 + ], + [ + 1507235590000, + 0 + ], + [ + 1507235600000, + 0 + ], + [ + 1507235610000, + 1.3333333333333333 + ], + [ + 1507235620000, + 1.1666666666666667 + ], + [ + 1507235630000, + 0 + ], + [ + 1507235640000, + 0 + ], + [ + 1507235650000, + 0 + ], + [ + 1507235660000, + 0 + ], + [ + 1507235670000, + 2.3333333333333335 + ], + [ + 1507235680000, + 2.8749999999999996 + ], + [ + 1507235690000, + 0 + ], + [ + 1507235700000, + 0 + ] + ] + } + ], + "node_jvm_mem": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.mem.heap_max_in_bytes", + "metricAgg": "max", + "label": "Max Heap", + "title": "JVM Heap", + "description": "Total heap available to Elasticsearch running in the JVM.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 709623808 + ], + [ + 1507235530000, + 709623808 + ], + [ + 1507235540000, + 709623808 + ], + [ + 1507235550000, + 709623808 + ], + [ + 1507235560000, + 709623808 + ], + [ + 1507235570000, + 709623808 + ], + [ + 1507235580000, + 709623808 + ], + [ + 1507235590000, + 709623808 + ], + [ + 1507235600000, + 709623808 + ], + [ + 1507235610000, + 709623808 + ], + [ + 1507235620000, + 709623808 + ], + [ + 1507235630000, + 709623808 + ], + [ + 1507235640000, + 709623808 + ], + [ + 1507235650000, + 709623808 + ], + [ + 1507235660000, + 709623808 + ], + [ + 1507235670000, + 709623808 + ], + [ + 1507235680000, + 709623808 + ], + [ + 1507235690000, + 709623808 + ], + [ + 1507235700000, + 709623808 + ] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.mem.heap_used_in_bytes", + "metricAgg": "max", + "label": "Used Heap", + "title": "JVM Heap", + "description": "Total heap used by Elasticsearch running in the JVM.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 317052776 + ], + [ + 1507235530000, + 344014976 + ], + [ + 1507235540000, + 368593248 + ], + [ + 1507235550000, + 253850400 + ], + [ + 1507235560000, + 348095032 + ], + [ + 1507235570000, + 182919712 + ], + [ + 1507235580000, + 212395016 + ], + [ + 1507235590000, + 244004144 + ], + [ + 1507235600000, + 270412240 + ], + [ + 1507235610000, + 245052864 + ], + [ + 1507235620000, + 370270616 + ], + [ + 1507235630000, + 196944168 + ], + [ + 1507235640000, + 223491760 + ], + [ + 1507235650000, + 253878472 + ], + [ + 1507235660000, + 280811736 + ], + [ + 1507235670000, + 371931976 + ], + [ + 1507235680000, + 329874616 + ], + [ + 1507235690000, + 363869776 + ], + [ + 1507235700000, + 211045968 + ] + ] + } + ], + "node_mem": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.memory_in_bytes", + "metricAgg": "max", + "label": "Lucene Total", + "title": "Index Memory", + "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 4797457 + ], + [ + 1507235530000, + 4797457 + ], + [ + 1507235540000, + 4797457 + ], + [ + 1507235550000, + 4797457 + ], + [ + 1507235560000, + 4823580 + ], + [ + 1507235570000, + 4823580 + ], + [ + 1507235580000, + 4823580 + ], + [ + 1507235590000, + 4823580 + ], + [ + 1507235600000, + 4823580 + ], + [ + 1507235610000, + 4838368 + ], + [ + 1507235620000, + 4741420 + ], + [ + 1507235630000, + 4741420 + ], + [ + 1507235640000, + 4741420 + ], + [ + 1507235650000, + 4741420 + ], + [ + 1507235660000, + 4741420 + ], + [ + 1507235670000, + 4757998 + ], + [ + 1507235680000, + 4787542 + ], + [ + 1507235690000, + 4787542 + ], + [ + 1507235700000, + 4787542 + ] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.terms_memory_in_bytes", + "metricAgg": "max", + "label": "Terms", + "title": "Index Memory", + "description": "Heap memory used by Terms (e.g., text). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 3764438 + ], + [ + 1507235530000, + 3764438 + ], + [ + 1507235540000, + 3764438 + ], + [ + 1507235550000, + 3764438 + ], + [ + 1507235560000, + 3786762 + ], + [ + 1507235570000, + 3786762 + ], + [ + 1507235580000, + 3786762 + ], + [ + 1507235590000, + 3786762 + ], + [ + 1507235600000, + 3786762 + ], + [ + 1507235610000, + 3799306 + ], + [ + 1507235620000, + 3715996 + ], + [ + 1507235630000, + 3715996 + ], + [ + 1507235640000, + 3715996 + ], + [ + 1507235650000, + 3715996 + ], + [ + 1507235660000, + 3715996 + ], + [ + 1507235670000, + 3729890 + ], + [ + 1507235680000, + 3755528 + ], + [ + 1507235690000, + 3755528 + ], + [ + 1507235700000, + 3755528 + ] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.points_memory_in_bytes", + "metricAgg": "max", + "label": "Points", + "title": "Index Memory", + "description": "Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 12171 + ], + [ + 1507235530000, + 12171 + ], + [ + 1507235540000, + 12171 + ], + [ + 1507235550000, + 12171 + ], + [ + 1507235560000, + 12198 + ], + [ + 1507235570000, + 12198 + ], + [ + 1507235580000, + 12198 + ], + [ + 1507235590000, + 12198 + ], + [ + 1507235600000, + 12198 + ], + [ + 1507235610000, + 12218 + ], + [ + 1507235620000, + 12120 + ], + [ + 1507235630000, + 12120 + ], + [ + 1507235640000, + 12120 + ], + [ + 1507235650000, + 12120 + ], + [ + 1507235660000, + 12120 + ], + [ + 1507235670000, + 12140 + ], + [ + 1507235680000, + 12166 + ], + [ + 1507235690000, + 12166 + ], + [ + 1507235700000, + 12166 + ] + ] + } + ], + "node_cpu_metric": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.process.cpu.percent", + "metricAgg": "max", + "label": "CPU Utilization", + "description": "Percentage of CPU usage for the Elasticsearch process.", + "units": "%", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 1 + ], + [ + 1507235530000, + 0 + ], + [ + 1507235540000, + 0 + ], + [ + 1507235550000, + 1 + ], + [ + 1507235560000, + 2 + ], + [ + 1507235570000, + 0 + ], + [ + 1507235580000, + 2 + ], + [ + 1507235590000, + 0 + ], + [ + 1507235600000, + 0 + ], + [ + 1507235610000, + 3 + ], + [ + 1507235620000, + 2 + ], + [ + 1507235630000, + 2 + ], + [ + 1507235640000, + 0 + ], + [ + 1507235650000, + 1 + ], + [ + 1507235660000, + 0 + ], + [ + 1507235670000, + 2 + ], + [ + 1507235680000, + 2 + ], + [ + 1507235690000, + 1 + ], + [ + 1507235700000, + 0 + ] + ] + } + ], + "node_load_average": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.os.cpu.load_average.1m", + "metricAgg": "max", + "label": "1m", + "title": "System Load", + "description": "Load average over the last minute.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 2.876953125 + ], + [ + 1507235530000, + 2.66015625 + ], + [ + 1507235540000, + 2.40625 + ], + [ + 1507235550000, + 2.189453125 + ], + [ + 1507235560000, + 2.626953125 + ], + [ + 1507235570000, + 2.451171875 + ], + [ + 1507235580000, + 2.81640625 + ], + [ + 1507235590000, + 3.70703125 + ], + [ + 1507235600000, + 3.51171875 + ], + [ + 1507235610000, + 3.359375 + ], + [ + 1507235620000, + 3.076171875 + ], + [ + 1507235630000, + 2.990234375 + ], + [ + 1507235640000, + 2.904296875 + ], + [ + 1507235650000, + 2.84375 + ], + [ + 1507235660000, + 3.28125 + ], + [ + 1507235670000, + 5.30859375 + ], + [ + 1507235680000, + 7.63671875 + ], + [ + 1507235690000, + 9.4375 + ], + [ + 1507235700000, + 11.421875 + ] + ] + } + ], + "node_segment_count": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.count", + "metricAgg": "max", + "label": "Segment Count", + "description": "Maximum segment count for primary and replica shards on this node.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1507235520000, + 128 + ], + [ + 1507235530000, + 128 + ], + [ + 1507235540000, + 128 + ], + [ + 1507235550000, + 128 + ], + [ + 1507235560000, + 131 + ], + [ + 1507235570000, + 131 + ], + [ + 1507235580000, + 131 + ], + [ + 1507235590000, + 131 + ], + [ + 1507235600000, + 131 + ], + [ + 1507235610000, + 133 + ], + [ + 1507235620000, + 126 + ], + [ + 1507235630000, + 126 + ], + [ + 1507235640000, + 126 + ], + [ + 1507235650000, + 126 + ], + [ + 1507235660000, + 126 + ], + [ + 1507235670000, + 128 + ], + [ + 1507235680000, + 130 + ], + [ + 1507235690000, + 130 + ], + [ + 1507235700000, + 130 + ] + ] + } + ] }, "logs": { "enabled": false, - "limit": 10, + "logs": [], "reason": { - "clusterExists": false, "indexPatternExists": false, "indexPatternInTimeRangeExists": false, "typeExistsAtAnyTime": false, + "typeExists": false, "usingStructuredLogs": false, + "clusterExists": false, "nodeExists": false, - "indexExists": null, - "typeExists": false + "indexExists": null }, - "logs": [] + "limit": 10 }, - "metrics": { - "node_latency": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.search.query_total", - "metricAgg": "sum", - "label": "Search", - "title": "Latency", - "description": "Average latency for searching, which is time it takes to execute searches divided by number of searches submitted. This considers primary and replica shards.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": true, - "isDerivative": false - }, - "data": [] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.indexing.index_total", - "metricAgg": "sum", - "label": "Indexing", - "title": "Latency", - "description": "Average latency for indexing documents, which is time it takes to index documents divided by number that were indexed. This considers any shard located on this node, including replicas.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": true, - "isDerivative": false - }, - "data": [] - }], - "node_jvm_mem": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.mem.heap_max_in_bytes", - "metricAgg": "max", - "label": "Max Heap", - "title": "JVM Heap", - "description": "Total heap available to Elasticsearch running in the JVM.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.mem.heap_used_in_bytes", - "metricAgg": "max", - "label": "Used Heap", - "title": "JVM Heap", - "description": "Total heap used by Elasticsearch running in the JVM.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }], - "node_mem": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.memory_in_bytes", - "metricAgg": "max", - "label": "Lucene Total", - "title": "Index Memory", - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.terms_memory_in_bytes", - "metricAgg": "max", - "label": "Terms", - "title": "Index Memory", - "description": "Heap memory used by Terms (e.g., text). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.points_memory_in_bytes", - "metricAgg": "max", - "label": "Points", - "title": "Index Memory", - "description": "Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }], - "node_cpu_metric": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.process.cpu.percent", - "metricAgg": "max", - "label": "CPU Utilization", - "description": "Percentage of CPU usage for the Elasticsearch process.", - "units": "%", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }], - "node_total_io": [{ - "bucket_size": "10 seconds", - "data": [], - "metric": { - "app": "elasticsearch", - "description": "Total I/O. (This metric is not supported on all platforms and may display N/A if I/O data is unavailable.)", - "field": "node_stats.fs.io_stats.total.operations", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true, - "label": "Total I/O", - "metricAgg": "max", - "title": "I/O Operations Rate", - "units": "/s" - }, - "timeRange": { - "max": 1507235712000, - "min": 1507235508000 - } - }, - { - "bucket_size": "10 seconds", - "data": [], - "metric": { - "app": "elasticsearch", - "description": "Total Read I/O. (This metric is not supported on all platforms and may display N/A if I/O data is unavailable.)", - "field": "node_stats.fs.io_stats.total.read_operations", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true, - "label": "Total Read I/O", - "metricAgg": "max", - "title": "I/O Operations Rate", - "units": "/s" - }, - "timeRange": { - "max": 1507235712000, - "min": 1507235508000 - } + "shards": [ + { + "index": "watermelon-tweets-2017.10.05", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 4, + "state": "STARTED" }, { - "bucket_size": "10 seconds", - "data": [], - "metric": { - "app": "elasticsearch", - "description": "Total Write I/O. (This metric is not supported on all platforms and may display N/A if I/O data is unavailable.)", - "field": "node_stats.fs.io_stats.total.write_operations", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true, - "label": "Total Write I/O", - "metricAgg": "max", - "title": "I/O Operations Rate", - "units": "/s" - }, - "timeRange": { - "max": 1507235712000, - "min": 1507235508000 - } - }], - "node_load_average": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.os.cpu.load_average.1m", - "metricAgg": "max", - "label": "1m", - "title": "System Load", - "description": "Load average over the last minute.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }], - "node_segment_count": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.count", - "metricAgg": "max", - "label": "Segment Count", - "description": "Maximum segment count for primary and replica shards on this node.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [] - }] - }, - "shards": [], + "index": "watermelon-tweets-2017.10.05", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 1, + "state": "STARTED" + }, + { + "index": "watermelon-tweets-2017.10.05", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 2, + "state": "STARTED" + }, + { + "index": "watermelon-tweets-2017.10.05", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 3, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.09.30", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 4, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.09.30", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 1, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.09.30", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": false, + "relocating_node": null, + "shard": 2, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.09.30", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 3, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.09.30", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": false, + "relocating_node": null, + "shard": 0, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.03", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 4, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.03", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 1, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.03", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 2, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.03", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 3, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.03", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 0, + "state": "STARTED" + }, + { + "index": "phone-home", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 4, + "state": "STARTED" + }, + { + "index": "phone-home", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 2, + "state": "STARTED" + }, + { + "index": "phone-home", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 3, + "state": "STARTED" + }, + { + "index": "phone-home", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 0, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.02", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 4, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.02", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": false, + "relocating_node": null, + "shard": 1, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.02", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": false, + "relocating_node": null, + "shard": 2, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.02", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 3, + "state": "STARTED" + }, + { + "index": "avocado-tweets-2017.10.02", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": null, + "shard": 0, + "state": "STARTED" + }, + { + "index": "relocation_test", + "node": "jUT5KdxfRbORSCWkb5zjmA", + "primary": true, + "relocating_node": "bwQWH-7IQY-mFPpfoaoFXQ", + "shard": 0, + "state": "RELOCATING" + } + ], "shardStats": { "indices": { "avocado-tweets-2017.09.30": { "status": "green", - "primary": 5, - "replica": 5, + "primary": 3, + "replica": 2, "unassigned": { "primary": 0, "replica": 0 @@ -295,8 +1520,8 @@ }, "avocado-tweets-2017.10.02": { "status": "green", - "primary": 5, - "replica": 5, + "primary": 3, + "replica": 2, "unassigned": { "primary": 0, "replica": 0 @@ -305,43 +1530,34 @@ "avocado-tweets-2017.10.03": { "status": "green", "primary": 5, - "replica": 5, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 } }, "phone-home": { - "status": "yellow", - "primary": 5, - "replica": 4, + "status": "green", + "primary": 4, + "replica": 0, "unassigned": { "primary": 0, - "replica": 1 + "replica": 0 } }, "watermelon-tweets-2017.10.05": { - "status": "yellow", - "primary": 5, - "replica": 4, - "unassigned": { - "primary": 0, - "replica": 1 - } - }, - ".security-v6": { - "status": "yellow", - "primary": 1, - "replica": 1, + "status": "green", + "primary": 4, + "replica": 0, "unassigned": { "primary": 0, - "replica": 1 + "replica": 0 } }, ".kibana": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -349,7 +1565,7 @@ }, ".monitoring-alerts-6": { "status": "green", - "primary": 1, + "primary": 0, "replica": 1, "unassigned": { "primary": 0, @@ -358,7 +1574,7 @@ }, ".monitoring-es-6-2017.10.05": { "status": "yellow", - "primary": 1, + "primary": 0, "replica": 0, "unassigned": { "primary": 0, @@ -367,17 +1583,26 @@ }, ".monitoring-kibana-6-2017.10.05": { "status": "yellow", - "primary": 1, + "primary": 0, "replica": 0, "unassigned": { "primary": 0, "replica": 1 } }, + ".security-v6": { + "status": "green", + "primary": 1, + "replica": 0, + "unassigned": { + "primary": 0, + "replica": 0 + } + }, ".triggered_watches": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -386,7 +1611,7 @@ ".watcher-history-7-2017.09.29": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -395,7 +1620,7 @@ ".watcher-history-7-2017.09.30": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -404,7 +1629,7 @@ ".watcher-history-7-2017.10.01": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -412,7 +1637,7 @@ }, ".watcher-history-7-2017.10.02": { "status": "green", - "primary": 1, + "primary": 0, "replica": 1, "unassigned": { "primary": 0, @@ -422,7 +1647,7 @@ ".watcher-history-7-2017.10.03": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -430,7 +1655,7 @@ }, ".watcher-history-7-2017.10.04": { "status": "green", - "primary": 1, + "primary": 0, "replica": 1, "unassigned": { "primary": 0, @@ -440,7 +1665,7 @@ ".watcher-history-7-2017.10.05": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -449,7 +1674,7 @@ ".watches": { "status": "green", "primary": 1, - "replica": 1, + "replica": 0, "unassigned": { "primary": 0, "replica": 0 @@ -471,22 +1696,10 @@ "shardCount": 38, "indexCount": 20, "name": "whatever-01", - "node_ids": ["jUT5KdxfRbORSCWkb5zjmA"], + "node_ids": [ + "jUT5KdxfRbORSCWkb5zjmA" + ], "type": "master" - }, - "xcP6ue7eRCieNNitFTT0EA": { - "shardCount": 36, - "indexCount": 19, - "name": "whatever-02", - "node_ids": ["xcP6ue7eRCieNNitFTT0EA"], - "type": "node" - }, - "bwQWH-7IQY-mFPpfoaoFXQ": { - "shardCount": 4, - "indexCount": 4, - "name": "whatever-03", - "node_ids": ["bwQWH-7IQY-mFPpfoaoFXQ"], - "type": "node" } }, "stateUuid": "6wwwErXyTfaa4uHBHG5Pbg" diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/node_detail.js b/x-pack/test/api_integration/apis/monitoring/elasticsearch/node_detail.js index 43b5f6d119d6b..9fbf1e02c9c2e 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/node_detail.js +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/node_detail.js @@ -32,7 +32,7 @@ export default function({ getService }) { it('should summarize node with metrics', async () => { const { body } = await supertest .post( - '/api/monitoring/v1/clusters/YCxj-RAgSZCP6GuOQ8M1EQ/elasticsearch/nodes/jxcP6ue7eRCieNNitFTT0EA' + '/api/monitoring/v1/clusters/YCxj-RAgSZCP6GuOQ8M1EQ/elasticsearch/nodes/jUT5KdxfRbORSCWkb5zjmA' ) .set('kbn-xsrf', 'xxx') .send({ diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/ccs.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/ccs.js new file mode 100644 index 0000000000000..4c32e311c6cf3 --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/ccs.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('ccs', () => { + const archive = 'monitoring/setup/collection/detect_apm'; + const timeRange = { + min: '2019-04-16T00:00:00.741Z', + max: '2019-04-16T23:59:59.741Z', + }; + + before('load archive', () => { + return esArchiver.load(archive); + }); + + after('unload archive', () => { + return esArchiver.unload(archive); + }); + + it('should not fail with a ccs parameter for cluster', async () => { + await supertest + .post('/api/monitoring/v1/setup/collection/cluster?skipLiveData=true') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, ccs: '*' }) + .expect(200); + }); + + it('should not fail with a ccs parameter for node', async () => { + await supertest + .post('/api/monitoring/v1/setup/collection/node/123?skipLiveData=true') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, ccs: '*' }) + .expect(200); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js index 48d8b15ecbcad..01594babbc2f4 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js @@ -16,5 +16,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./detect_logstash_management')); loadTestFile(require.resolve('./detect_apm')); loadTestFile(require.resolve('./security')); + loadTestFile(require.resolve('./ccs')); }); } 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 0bf24be0fa0b0..db705b301a71e 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -79,9 +79,16 @@ export default function({ getService }) { expect(stats.stack_stats.kibana.plugins.apm.services_per_agent).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.infraops.last_24_hours).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['maps-telemetry'].attributes.timeCaptured).to.be.a( + 'string' + ); + expect(stats.stack_stats.kibana.plugins.reporting.enabled).to.be(true); expect(stats.stack_stats.kibana.plugins.rollups.index_patterns).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.spaces.available).to.be(true); + expect(stats.stack_stats.kibana.plugins.fileUploadTelemetry.filesUploadedTotalCount).to.be.a( + 'number' + ); expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); diff --git a/x-pack/test/api_integration/apis/uptime/graphql/filter_bar.js b/x-pack/test/api_integration/apis/uptime/graphql/filter_bar.js deleted file mode 100644 index c21a602d7ba1a..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/filter_bar.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { expectFixtureEql } from './helpers/expect_fixture_eql'; -import { filterBarQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; - -export default function({ getService }) { - describe('filterBar query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('returns the expected filters', async () => { - const getFilterBarQuery = { - operationName: 'FilterBar', - query: filterBarQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getFilterBarQuery }); - expectFixtureEql(data, 'filter_list'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filter_list.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filter_list.json deleted file mode 100644 index f07e416322105..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filter_list.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "filterBar": { - "ids": [ - "0000-intermittent", - "0001-up", - "0002-up", - "0003-up", - "0004-up", - "0005-up", - "0006-up", - "0007-up", - "0008-up", - "0009-up", - "0010-down", - "0011-up", - "0012-up", - "0013-up", - "0014-up", - "0015-intermittent", - "0016-up", - "0017-up", - "0018-up", - "0019-up" - ], - "locations": [ - "mpls" - ], - "ports": [ - 5678 - ], - "schemes": [ - "http" - ], - "urls": [ - "http://localhost:5678/pattern?r=200x1", - "http://localhost:5678/pattern?r=200x5,500x1", - "http://localhost:5678/pattern?r=400x1" - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filters.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filters.json new file mode 100644 index 0000000000000..76e307f27d841 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filters.json @@ -0,0 +1,12 @@ +{ + "schemes": [ + "http" + ], + "ports": [ + 5678 + ], + "locations": [ + "mpls" + ], + "tags": [] +} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_page_title.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_page_title.json deleted file mode 100644 index c0dac6c3711bc..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_page_title.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "monitorPageTitle": { - "id": "0002-up", - "url": "http://localhost:5678/pattern?r=200x1", - "name": "" - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status_by_id.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status_by_id.json deleted file mode 100644 index 8899803365595..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status_by_id.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "timestamp": "2019-09-11T03:40:34.371Z", - "monitor": { - "status": "up", - "duration": { - "us": 24627 - } - }, - "observer": { - "geo": { - "name": "mpls" - } - }, - "tls": null, - "url": { - "full": "http://localhost:5678/pattern?r=200x1" - } - } -] \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json index 93d63bad66e30..12d8f514a3a30 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json @@ -1,6 +1,5 @@ { - "up": 93, + "up": 10, "down": 7, - "mixed": 0, - "total": 100 + "total": 17 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json index 94c1ffbc74290..c1e7f0ba247fb 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json @@ -1,6 +1,5 @@ { "up": 0, - "down": 7, - "mixed": 0, - "total": 7 + "down": 0, + "total": 0 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json index 94c1ffbc74290..94777570dd6f0 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json @@ -1,6 +1,5 @@ { "up": 0, "down": 7, - "mixed": 0, "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json index 2d79880e7c0ee..42a1581707360 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json @@ -1,6 +1,5 @@ { - "up": 93, + "up": 10, "down": 0, - "mixed": 0, - "total": 93 + "total": 10 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts b/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts index 362803d2b8550..45cc9011773a9 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts @@ -10,6 +10,7 @@ import { join } from 'path'; import { cloneDeep } from 'lodash'; const fixturesDir = join(__dirname, '..', 'fixtures'); +const restFixturesDir = join(__dirname, '../../rest/', 'fixtures'); const excludeFieldsFrom = (from: any, excluder?: (d: any) => any): any => { const clone = cloneDeep(from); @@ -20,7 +21,11 @@ const excludeFieldsFrom = (from: any, excluder?: (d: any) => any): any => { }; export const expectFixtureEql = (data: T, fixtureName: string, excluder?: (d: T) => void) => { - const fixturePath = join(fixturesDir, `${fixtureName}.json`); + let fixturePath = join(fixturesDir, `${fixtureName}.json`); + if (!fs.existsSync(fixturePath)) { + fixturePath = join(restFixturesDir, `${fixtureName}.json`); + } + const dataExcluded = excludeFieldsFrom(data, excluder); expect(dataExcluded).not.to.be(undefined); if (process.env.UPDATE_UPTIME_FIXTURES) { diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts index 9bfdf04c8dbe3..f89905f0da04f 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts @@ -5,11 +5,12 @@ */ import uuid from 'uuid'; -import { merge } from 'lodash'; +import { merge, flattenDeep } from 'lodash'; + +const INDEX_NAME = 'heartbeat-8.0.0'; export const makePing = async ( es: any, - index: string, monitorId: string, fields: { [key: string]: any }, mogrify: (doc: any) => any @@ -101,7 +102,7 @@ export const makePing = async ( const doc = mogrify(merge(baseDoc, fields)); await es.index({ - index, + index: INDEX_NAME, refresh: true, body: doc, }); @@ -111,7 +112,6 @@ export const makePing = async ( export const makeCheck = async ( es: any, - index: string, monitorId: string, numIps: number, fields: { [key: string]: any }, @@ -137,7 +137,7 @@ export const makeCheck = async ( if (i === numIps - 1) { pingFields.summary = summary; } - const doc = await makePing(es, index, monitorId, pingFields, mogrify); + const doc = await makePing(es, monitorId, pingFields, mogrify); docs.push(doc); // @ts-ignore summary[doc.monitor.status]++; @@ -147,16 +147,78 @@ export const makeCheck = async ( export const makeChecks = async ( es: any, - index: string, monitorId: string, numChecks: number, numIps: number, + every: number, // number of millis between checks fields: { [key: string]: any } = {}, mogrify: (doc: any) => any = d => d ) => { const checks = []; + const oldestTime = new Date().getTime() - numChecks * every; + let newestTime = oldestTime; for (let li = 0; li < numChecks; li++) { - checks.push(await makeCheck(es, index, monitorId, numIps, fields, mogrify)); + const checkDate = new Date(newestTime + every); + newestTime = checkDate.getTime() + every; + fields = merge(fields, { + '@timestamp': checkDate.toISOString(), + monitor: { + timespan: { + gte: checkDate.toISOString(), + lt: new Date(newestTime).toISOString(), + }, + }, + }); + checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify)); } + return checks; }; + +export const makeChecksWithStatus = async ( + es: any, + monitorId: string, + numChecks: number, + numIps: number, + every: number, + fields: { [key: string]: any } = {}, + status: 'up' | 'down', + mogrify: (doc: any) => any = d => d +) => { + const oppositeStatus = status === 'up' ? 'down' : 'up'; + + return await makeChecks(es, monitorId, numChecks, numIps, every, fields, d => { + d.monitor.status = status; + if (d.summary) { + d.summary[status] += d.summary[oppositeStatus]; + d.summary[oppositeStatus] = 0; + } + + return mogrify(d); + }); +}; + +// Helper for processing a list of checks to find the time picker bounds. +export const getChecksDateRange = (checks: any[]) => { + // Flatten 2d arrays + const flattened = flattenDeep(checks); + + let startTime = 1 / 0; + let endTime = -1 / 0; + flattened.forEach(c => { + const ts = Date.parse(c['@timestamp']); + + if (ts < startTime) { + startTime = ts; + } + + if (ts > endTime) { + endTime = ts; + } + }); + + return { + start: new Date(startTime).toISOString(), + end: new Date(endTime).toISOString(), + }; +}; diff --git a/x-pack/test/api_integration/apis/uptime/graphql/index.js b/x-pack/test/api_integration/apis/uptime/graphql/index.js index 3a696244a5eba..64999761fde4e 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/index.js +++ b/x-pack/test/api_integration/apis/uptime/graphql/index.js @@ -11,11 +11,8 @@ export default function({ loadTestFile }) { // verifying the pre-loaded documents are returned in a way that // matches the snapshots contained in './fixtures' loadTestFile(require.resolve('./doc_count')); - loadTestFile(require.resolve('./filter_bar')); loadTestFile(require.resolve('./monitor_charts')); - loadTestFile(require.resolve('./monitor_page_title')); loadTestFile(require.resolve('./monitor_states')); - loadTestFile(require.resolve('./monitor_status_bar')); loadTestFile(require.resolve('./ping_list')); loadTestFile(require.resolve('./snapshot_histogram')); }); diff --git a/x-pack/test/api_integration/apis/uptime/graphql/monitor_page_title.ts b/x-pack/test/api_integration/apis/uptime/graphql/monitor_page_title.ts deleted file mode 100644 index 56d22610ffe49..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/monitor_page_title.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { monitorPageTitleQueryString } from '../../../../../legacy/plugins/uptime/public/queries/monitor_page_title_query'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; - -export default function({ getService }: FtrProviderContext) { - describe('monitor_page_title', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('will fetch a title for a given monitorId', async () => { - const getMonitorTitleQuery = { - operationName: 'MonitorPageTitle', - query: monitorPageTitleQueryString, - variables: { - monitorId: '0002-up', - }, - }; - - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getMonitorTitleQuery }); - - expectFixtureEql(data, 'monitor_page_title'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts b/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts index c305bb99c28f7..511cdb6d004fa 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { monitorStatesQueryString } from '../../../../../legacy/plugins/uptime/public/queries/monitor_states_query'; import { expectFixtureEql } from './helpers/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { makeChecks } from './helpers/make_checks'; +import { makeChecksWithStatus } from './helpers/make_checks'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -104,11 +104,10 @@ export default function({ getService }: FtrProviderContext) { }; before(async () => { - const index = 'heartbeat-8.0.0'; - const es = getService('legacyEs'); dateRangeStart = new Date().toISOString(); - checks = await makeChecks(es, index, testMonitorId, 1, numIps, {}, d => { + checks = await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', d => { + // turn an all up status into having at least one down if (d.summary) { d.monitor.status = 'down'; d.summary.up--; diff --git a/x-pack/test/api_integration/apis/uptime/graphql/monitor_status_bar.js b/x-pack/test/api_integration/apis/uptime/graphql/monitor_status_bar.js deleted file mode 100644 index 5d03c90bb2032..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/monitor_status_bar.js +++ /dev/null @@ -1,58 +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 { monitorStatusBarQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; - -export default function({ getService }) { - describe('monitorStatusBar query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('returns the status for all monitors with no ID filtering', async () => { - const getMonitorStatusBarQuery = { - operationName: 'MonitorStatus', - query: monitorStatusBarQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { - data: { monitorStatus: responseData }, - }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getMonitorStatusBarQuery }); - - expectFixtureEql(responseData, 'monitor_status_all', res => - res.forEach(i => delete i.millisFromNow) - ); - }); - - it('returns the status for only the given monitor', async () => { - const getMonitorStatusBarQuery = { - operationName: 'MonitorStatus', - query: monitorStatusBarQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - monitorId: '0002-up', - }, - }; - const res = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getMonitorStatusBarQuery }); - - expectFixtureEql(res.body.data.monitorStatus, 'monitor_status_by_id'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/rest/filters.ts b/x-pack/test/api_integration/apis/uptime/rest/filters.ts new file mode 100644 index 0000000000000..6cec6143a6d7c --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/filters.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const getApiPath = (dateRangeStart: string, dateRangeEnd: string, filters?: string) => + `/api/uptime/filters?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}${ + filters ? `&filters=${filters}` : '' + }`; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('filter group endpoint', () => { + const dateRangeStart = '2019-01-28T17:40:08.078Z'; + const dateRangeEnd = '2025-01-28T19:00:16.078Z'; + + it('returns expected filters', async () => { + const resp = await supertest.get(getApiPath(dateRangeStart, dateRangeEnd)); + expectFixtureEql(resp.body, 'filters'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json new file mode 100644 index 0000000000000..1702cb2c21007 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json @@ -0,0 +1,89 @@ +{ + "@timestamp": "2019-09-11T03:40:34.371Z", + "agent": { + "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", + "hostname": "avc-x1x", + "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", + "type": "heartbeat", + "version": "8.0.0" + }, + "ecs": { + "version": "1.1.0" + }, + "event": { + "dataset": "uptime" + }, + "host": { + "name": "avc-x1x" + }, + "http": { + "response": { + "body": { + "bytes": 3, + "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf" + }, + "status_code": 200 + }, + "rtt": { + "content": { + "us": 57 + }, + "response_header": { + "us": 262 + }, + "total": { + "us": 20331 + }, + "validate": { + "us": 319 + }, + "write_request": { + "us": 82 + } + } + }, + "monitor": { + "check_group": "d76f0762-d445-11e9-88e3-3e80641b9c71", + "duration": { + "us": 24627 + }, + "id": "0002-up", + "ip": "127.0.0.1", + "name": "", + "status": "up", + "type": "http" + }, + "observer": { + "geo": { + "location": "37.926868, -78.024902", + "name": "mpls" + }, + "hostname": "avc-x1x" + }, + "resolve": { + "ip": "127.0.0.1", + "rtt": { + "us": 4218 + } + }, + "summary": { + "down": 0, + "up": 1 + }, + "tcp": { + "rtt": { + "connect": { + "us": 103 + } + } + }, + "timestamp": "2019-09-11T03:40:34.371Z", + "url": { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=200x1", + "path": "/pattern", + "port": 5678, + "query": "r=200x1", + "scheme": "http" + } + } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/selected_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/selected_monitor.json new file mode 100644 index 0000000000000..d8367ea67052f --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/selected_monitor.json @@ -0,0 +1,28 @@ +{ + "monitor": { + "check_group": "d76f0762-d445-11e9-88e3-3e80641b9c71", + "duration": { + "us": 24627 + }, + "id": "0002-up", + "ip": "127.0.0.1", + "name": "", + "status": "up", + "type": "http" + }, + "observer": { + "geo": { + "location": "37.926868, -78.024902", + "name": "mpls" + }, + "hostname": "avc-x1x" + }, + "url": { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=200x1", + "path": "/pattern", + "port": 5678, + "query": "r=200x1", + "scheme": "http" + } +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index b76d3f7c2e44a..a86411f7c49ec 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -9,8 +9,16 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('uptime REST endpoints', () => { - before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat')); - after('unload', () => esArchiver.unload('uptime/full_heartbeat')); - loadTestFile(require.resolve('./snapshot')); + describe('with generated data', () => { + before('load heartbeat data', () => esArchiver.load('uptime/blank')); + after('unload', () => esArchiver.unload('uptime/blank')); + loadTestFile(require.resolve('./snapshot')); + }); + describe('with real-world data', () => { + before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat')); + after('unload', () => esArchiver.unload('uptime/full_heartbeat')); + loadTestFile(require.resolve('./monitor_latest_status')); + loadTestFile(require.resolve('./selected_monitor')); + }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts new file mode 100644 index 0000000000000..749b304c87ee3 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + describe('get monitor latest status API', () => { + const dateStart = '2018-01-28T17:40:08.078Z'; + const dateEnd = '2025-01-28T19:00:16.078Z'; + const monitorId = '0002-up'; + + const supertest = getService('supertest'); + + it('returns the status for only the given monitor', async () => { + const apiResponse = await supertest.get( + `/api/uptime/monitor/status?monitorId=${monitorId}&dateStart=${dateStart}&dateEnd=${dateEnd}` + ); + expectFixtureEql(apiResponse.body, 'monitor_latest_status'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/selected_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/selected_monitor.ts new file mode 100644 index 0000000000000..ed034f58a5f59 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/selected_monitor.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 { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + describe('get selected monitor by ID', () => { + const monitorId = '0002-up'; + + const supertest = getService('supertest'); + + it('returns the monitor for give ID', async () => { + const apiResponse = await supertest.get( + `/api/uptime/monitor/selected?monitorId=${monitorId}` + ); + expectFixtureEql(apiResponse.body, 'selected_monitor'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts index 0175dc649b495..b0d97837c770f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -6,47 +6,97 @@ import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { makeChecksWithStatus, getChecksDateRange } from '../graphql/helpers/make_checks'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('snapshot count', () => { - let dateRangeStart = '2019-01-28T17:40:08.078Z'; - let dateRangeEnd = '2025-01-28T19:00:16.078Z'; - - it('will fetch the full set of snapshot counts', async () => { - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` - ); - expectFixtureEql(apiResponse.body, 'snapshot'); - }); + const dateRangeStart = new Date().toISOString(); + const dateRangeEnd = new Date().toISOString(); - it('will fetch a monitor snapshot filtered by down status', async () => { - const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; - const statusFilter = 'down'; - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` - ); - expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); - }); + describe('when no data is present', async () => { + it('returns a null snapshot', async () => { + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + ); - it('will fetch a monitor snapshot filtered by up status', async () => { - const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`; - const statusFilter = 'up'; - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` - ); - expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + expectFixtureEql(apiResponse.body, 'snapshot_empty'); + }); }); - it('returns a null snapshot when no data is present', async () => { - dateRangeStart = '2019-01-25T04:30:54.740Z'; - dateRangeEnd = '2025-01-28T04:50:54.740Z'; - const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}` - ); - expectFixtureEql(apiResponse.body, 'snapshot_empty'); + describe('when data is present', async () => { + const numUpMonitors = 10; + const numDownMonitors = 7; + const numIps = 2; + const checksPerMonitor = 5; + const scheduleEvery = 10000; // fake monitor checks every 10s + let dateRange: { start: string; end: string }; + + [true, false].forEach(async (includeTimespan: boolean) => { + describe(`with timespans ${includeTimespan ? 'included' : 'missing'}`, async () => { + before(async () => { + const promises: Array> = []; + + // When includeTimespan is false we have to remove the values there. + let mogrify = (d: any) => d; + if ((includeTimespan = false)) { + mogrify = (d: any): any => { + d.monitor.delete('timespan'); + return d; + }; + } + + const makeMonitorChecks = async (monitorId: string, status: 'up' | 'down') => { + return makeChecksWithStatus( + getService('legacyEs'), + monitorId, + checksPerMonitor, + numIps, + scheduleEvery, + {}, + status, + mogrify + ); + }; + + for (let i = 0; i < numUpMonitors; i++) { + promises.push(makeMonitorChecks(`up-${i}`, 'up')); + } + for (let i = 0; i < numDownMonitors; i++) { + promises.push(makeMonitorChecks(`down-${i}`, 'down')); + } + + const allResults = await Promise.all(promises); + dateRange = getChecksDateRange(allResults); + }); + + it('will count all statuses correctly', async () => { + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}` + ); + + expectFixtureEql(apiResponse.body, 'snapshot'); + }); + + it('will fetch a monitor snapshot filtered by down status', async () => { + const statusFilter = 'down'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}` + ); + + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); + }); + + it('will fetch a monitor snapshot filtered by up status', async () => { + const statusFilter = 'up'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + }); + }); + }); }); }); } diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index f148d62421ff8..ad4f81777e780 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -67,9 +67,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Management']); }); @@ -125,9 +123,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows Management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Management']); }); @@ -178,9 +174,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows Management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Discover', 'Management']); }); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 9ac6d4fdef19f..ee58be76928b3 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -40,9 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Management'); }); diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 9190e0b4886ce..e2d5efac4644c 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -60,10 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link: Record) => link.text)).to.eql([ - 'APM', - 'Management', - ]); + expect(navLinks.map(link => link.text)).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { @@ -111,9 +108,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows apm navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['APM', 'Management']); }); @@ -166,9 +161,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show APM navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('APM'); }); diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts index 191ba5c4d1e25..1ac1784e0e05d 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -30,9 +30,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('APM'); }); @@ -61,9 +59,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('APM'); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index a58eb61ec4ca2..d0e37ec8e3f35 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -65,9 +65,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows canvas navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Canvas', 'Management']); }); @@ -143,9 +141,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows canvas navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Canvas', 'Management']); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 5a6857901536f..28b572401892b 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -40,9 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Canvas'); }); @@ -98,9 +96,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Canvas'); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index aa6860b35763f..d25fae3c4894c 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -75,10 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link: Record) => link.text)).to.eql([ - 'Dashboard', - 'Management', - ]); + expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Management']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -255,9 +252,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows dashboard navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Dashboard', 'Management']); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index c1197fa7023c5..ebe08a60c2563 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -43,9 +43,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dashboard'); }); @@ -107,9 +105,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Dashboard'); }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index fd7739e6930d0..494fd71ea6f34 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -63,10 +63,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link: Record) => link.text)).to.eql([ - 'Dev Tools', - 'Management', - ]); + expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { @@ -146,9 +143,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`shows 'Dev Tools' navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Dev Tools', 'Management']); }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts index e3bc3a1c6ce11..4184d223a9686 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -40,9 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dev Tools'); }); @@ -79,9 +77,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Dev Tools'); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 553ce459ebb18..1912b16d96f36 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -81,10 +81,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link: Record) => link.text)).to.eql([ - 'Discover', - 'Management', - ]); + expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Management']); }); it('shows save button', async () => { @@ -170,9 +167,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows discover navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Discover', 'Management']); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index 3e5dcd7b0c987..e6b6f28f8b92f 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -49,9 +49,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Discover'); }); @@ -93,9 +91,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Discover'); }); diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts index 1d1fb566eb075..d8eb969b99b3b 100644 --- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -30,9 +30,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('EEndpoint'); }); @@ -70,9 +68,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('EEndpoint'); }); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index acc8943033a1a..a2b062e6ef84f 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,10 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link: Record) => link.text)).to.eql([ - 'Graph', - 'Management', - ]); + expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Management']); }); it('landing page shows "Create new graph" button', async () => { @@ -129,9 +126,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows graph navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Graph', 'Management']); }); @@ -183,9 +178,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show graph navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Graph'); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts index 0945b35ba0930..a0b0d5bef9668 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -34,9 +34,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Graph'); }); @@ -75,9 +73,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Graph'); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 4929bb52c170c..30cdc95b38e62 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -69,9 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Management']); }); @@ -125,9 +123,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Management']); }); @@ -179,9 +175,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows Management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Discover', 'Management']); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index bc8542288410c..6a2b77de17f45 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -40,9 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Management'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index 4d61e0996419c..5062f094061c0 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -60,9 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows metrics navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Metrics', 'Management']); }); @@ -175,9 +173,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows metrics navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Metrics', 'Management']); }); @@ -417,9 +413,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show metrics navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.not.contain(['Metrics']); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 300b22e5bcbc3..7c2a11a542d66 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -48,9 +48,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Metrics'); }); @@ -101,9 +99,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Metrics'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index d092e6736656e..b9634c29dac1c 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -57,9 +57,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows logs navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Logs', 'Management']); }); @@ -122,9 +120,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows logs navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Logs', 'Management']); }); @@ -187,9 +183,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show logs navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.not.contain('Logs'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 8230b25efbbf9..6b078d2cfa71a 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -36,9 +36,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Logs'); }); @@ -77,9 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.not.contain('Logs'); }); diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index d1ae7138ecc65..639b65ec5eca8 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -22,7 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { state: undefined, }; const expectedSearchString = - "logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1565707203194))&sourceId=default"; + "logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1565707203194),streamLive:!f)&sourceId=default"; const expectedRedirectPath = '/logs/stream?'; await pageObjects.common.navigateToActualUrl( diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index 8b2df502dc100..8fb6f21c778d3 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -80,9 +80,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show ml navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Machine Learning'); }); }); @@ -103,9 +101,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows ML navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Machine Learning'); }); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index 13036737218bc..fc94688e98811 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -39,9 +39,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Machine Learning'); }); @@ -71,9 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Machine Learning'); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index cf31f445a96f3..804ad5725edfd 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -65,9 +65,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows maps navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Maps', 'Management']); }); @@ -154,9 +152,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows Maps navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Maps', 'Management']); }); @@ -251,9 +247,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('does not show Maps navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Discover', 'Management']); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts index 0c86b47b373e6..e157586aecead 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts @@ -42,9 +42,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Maps'); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index 8848df83d36d6..d985da42ab5ed 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -76,9 +76,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show monitoring navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Stack Monitoring'); }); }); @@ -99,9 +97,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows monitoring navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 80f33ff6175c5..7459b53ca4a32 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -41,9 +41,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); @@ -74,9 +72,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/security/basic_license/index.ts b/x-pack/test/functional/apps/security/basic_license/index.ts new file mode 100644 index 0000000000000..0dbbd3988f8dd --- /dev/null +++ b/x-pack/test/functional/apps/security/basic_license/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('security app - basic license', function() { + this.tags('ciGroup4'); + + loadTestFile(require.resolve('./role_mappings')); + }); +} diff --git a/x-pack/test/functional/apps/security/basic_license/role_mappings.ts b/x-pack/test/functional/apps/security/basic_license/role_mappings.ts new file mode 100644 index 0000000000000..45b325d57bee0 --- /dev/null +++ b/x-pack/test/functional/apps/security/basic_license/role_mappings.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'roleMappings']); + const testSubjects = getService('testSubjects'); + + describe('Role Mappings', function() { + before(async () => { + await pageObjects.common.navigateToApp('settings'); + }); + + it('does not render the Role Mappings UI under the basic license', async () => { + await testSubjects.missingOrFail('roleMappings'); + }); + }); +}; diff --git a/x-pack/test/functional/apps/security/index.js b/x-pack/test/functional/apps/security/index.js index b5d9b5f14be97..827a702b92d85 100644 --- a/x-pack/test/functional/apps/security/index.js +++ b/x-pack/test/functional/apps/security/index.js @@ -16,5 +16,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./field_level_security')); loadTestFile(require.resolve('./rbac_phase1')); loadTestFile(require.resolve('./user_email')); + loadTestFile(require.resolve('./role_mappings')); }); } diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index 361bb6dbbdae4..9309e23bdd762 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -7,7 +7,14 @@ import expect from '@kbn/expect'; import { indexBy } from 'lodash'; export default function({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['security', 'settings', 'common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects([ + 'security', + 'settings', + 'common', + 'visualize', + 'timePicker', + 'visChart', + ]); const log = getService('log'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); @@ -110,7 +117,7 @@ export default function({ getService, getPageObjects }) { '"' ); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visualize.waitForVisualization(); + await PageObjects.visChart.waitForVisualization(); await PageObjects.visualize.saveVisualizationExpectSuccess(vizName1); await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts new file mode 100644 index 0000000000000..5fed56ee79e3d --- /dev/null +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { parse } from 'url'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'roleMappings']); + const security = getService('security'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const aceEditor = getService('aceEditor'); + + describe('Role Mappings', function() { + before(async () => { + await pageObjects.common.navigateToApp('roleMappings'); + }); + + it('displays a message when no role mappings exist', async () => { + await testSubjects.existOrFail('roleMappingsEmptyPrompt'); + await testSubjects.existOrFail('createRoleMappingButton'); + }); + + it('allows a role mapping to be created', async () => { + await testSubjects.click('createRoleMappingButton'); + await testSubjects.setValue('roleMappingFormNameInput', 'new_role_mapping'); + await testSubjects.setValue('roleMappingFormRoleComboBox', 'superuser'); + await browser.pressKeys(browser.keys.ENTER); + + await testSubjects.click('roleMappingsAddRuleButton'); + + await testSubjects.click('roleMappingsJSONRuleEditorButton'); + + await aceEditor.setValue( + 'roleMappingsJSONEditor', + JSON.stringify({ + all: [ + { + field: { + username: '*', + }, + }, + { + field: { + 'metadata.foo.bar': 'baz', + }, + }, + { + except: { + any: [ + { + field: { + dn: 'foo', + }, + }, + { + field: { + dn: 'bar', + }, + }, + ], + }, + }, + ], + }) + ); + + await testSubjects.click('roleMappingsVisualRuleEditorButton'); + + await testSubjects.click('saveRoleMappingButton'); + + await testSubjects.existOrFail('savedRoleMappingSuccessToast'); + }); + + it('allows a role mapping to be deleted', async () => { + await testSubjects.click(`deleteRoleMappingButton-new_role_mapping`); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.existOrFail('deletedRoleMappingSuccessToast'); + }); + + it('displays an error and returns to the listing page when navigating to a role mapping which does not exist', async () => { + await pageObjects.common.navigateToActualUrl( + 'kibana', + '#/management/security/role_mappings/edit/i-do-not-exist', + { ensureCurrentUrl: false } + ); + + await testSubjects.existOrFail('errorLoadingRoleMappingEditorToast'); + + const url = parse(await browser.getCurrentUrl()); + + expect(url.hash).to.eql('#/management/security/role_mappings?_g=()'); + }); + + describe('with role mappings', () => { + const mappings = [ + { + name: 'a_enabled_role_mapping', + enabled: true, + roles: ['superuser'], + rules: { + field: { + username: '*', + }, + }, + metadata: {}, + }, + { + name: 'b_disabled_role_mapping', + enabled: false, + role_templates: [{ template: { source: 'superuser' } }], + rules: { + field: { + username: '*', + }, + }, + metadata: {}, + }, + ]; + + before(async () => { + await Promise.all( + mappings.map(mapping => { + const { name, ...payload } = mapping; + return security.roleMappings.create(name, payload); + }) + ); + + await pageObjects.common.navigateToApp('roleMappings'); + }); + + after(async () => { + await Promise.all(mappings.map(mapping => security.roleMappings.delete(mapping.name))); + }); + + it('displays a table of all role mappings', async () => { + const rows = await testSubjects.findAll('roleMappingRow'); + expect(rows.length).to.eql(mappings.length); + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const mapping = mappings[i]; + + const name = await ( + await testSubjects.findDescendant('roleMappingName', row) + ).getVisibleText(); + + const enabled = + (await ( + await testSubjects.findDescendant('roleMappingEnabled', row) + ).getVisibleText()) === 'Enabled'; + + expect(name).to.eql(mapping.name); + expect(enabled).to.eql(mapping.enabled); + } + }); + + it('allows a role mapping to be edited', async () => { + await testSubjects.click('roleMappingName'); + await testSubjects.click('saveRoleMappingButton'); + await testSubjects.existOrFail('savedRoleMappingSuccessToast'); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index 99d3ea7834e6b..608c7f321a08f 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'snapshotRestore']); const log = getService('log'); + const es = getService('legacyEs'); describe('Home page', function() { this.tags('smoke'); @@ -26,5 +27,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const repositoriesButton = await pageObjects.snapshotRestore.registerRepositoryButton(); expect(await repositoriesButton.isDisplayed()).to.be(true); }); + + describe('Repositories Tab', async () => { + before(async () => { + await es.snapshot.createRepository({ + repository: 'my-repository', + body: { + type: 'fs', + settings: { + location: '/tmp/es-backups/', + compress: true, + }, + }, + verify: true, + }); + await pageObjects.snapshotRestore.navToRepositories(); + }); + + it('cleanup repository', async () => { + await pageObjects.snapshotRestore.viewRepositoryDetails('my-repository'); + await pageObjects.common.sleep(25000); + const cleanupResponse = await pageObjects.snapshotRestore.performRepositoryCleanup(); + await pageObjects.common.sleep(25000); + expect(cleanupResponse).to.contain('results'); + expect(cleanupResponse).to.contain('deleted_bytes'); + expect(cleanupResponse).to.contain('deleted_blobs'); + }); + after(async () => { + await es.snapshot.deleteRepository({ + repository: 'my-repository', + }); + }); + }); }); }; diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 46f0be1e6f6d6..1e79c76bf83e5 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -55,9 +55,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Management'); }); @@ -131,9 +129,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows management navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Management'); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index 64fb218a62c80..dea45f161e451 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -59,9 +59,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows timelion navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Timelion', 'Management']); }); @@ -113,9 +111,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows timelion navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Timelion', 'Management']); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index ea5e255071dad..fb203a23359bd 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -38,9 +38,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Timelion'); }); @@ -71,9 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Timelion'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index c5a597cdaffb0..a004f8db66823 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -64,10 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link: Record) => link.text)).to.eql([ - 'Uptime', - 'Management', - ]); + expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { @@ -117,9 +114,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows uptime navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Uptime', 'Management']); }); @@ -170,9 +165,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show uptime navlink`, async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Uptime'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts index 96bc3c5f74f59..77c5b323340bf 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -30,9 +30,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Uptime'); }); @@ -59,9 +57,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Uptime'); }); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 9a337c86185fe..bcfb72967b75a 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -29,6 +29,27 @@ export default ({ getPageObjects }: FtrProviderContext) => { await pageObjects.uptime.pageHasExpectedIds(['0000-intermittent']); }); + it('applies filters for multiple fields', async () => { + await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); + await pageObjects.uptime.selectFilterItems({ + location: ['mpls'], + port: ['5678'], + scheme: ['http'], + }); + await pageObjects.uptime.pageHasExpectedIds([ + '0000-intermittent', + '0001-up', + '0002-up', + '0003-up', + '0004-up', + '0005-up', + '0006-up', + '0007-up', + '0008-up', + '0009-up', + ]); + }); + it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); @@ -64,5 +85,27 @@ export default ({ getPageObjects }: FtrProviderContext) => { '0009-up', ]); }); + + describe('snapshot counts', () => { + it('updates the snapshot count when status filter is set to down', async () => { + await pageObjects.uptime.goToUptimePageAndSetDateRange( + DEFAULT_DATE_START, + DEFAULT_DATE_END + ); + await pageObjects.uptime.setStatusFilter('down'); + const counts = await pageObjects.uptime.getSnapshotCount(); + expect(counts).to.eql({ up: '0', down: '7' }); + }); + + it('updates the snapshot count when status filter is set to up', async () => { + await pageObjects.uptime.goToUptimePageAndSetDateRange( + DEFAULT_DATE_START, + DEFAULT_DATE_END + ); + await pageObjects.uptime.setStatusFilter('up'); + const counts = await pageObjects.uptime.getSnapshotCount(); + expect(counts).to.eql({ up: '93', down: '0' }); + }); + }); }); }; diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 86fe606ecafad..d55076cb0ab43 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -74,9 +74,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows visualize navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Visualize', 'Management']); }); @@ -190,9 +188,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows visualize navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.eql(['Visualize', 'Management']); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index d0fdc7c95ea38..9193862d2ba9e 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -40,9 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Visualize'); }); @@ -81,9 +79,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - const navLinks = (await appsMenu.readLinks()).map( - (link: Record) => link.text - ); + const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).not.to.contain('Visualize'); }); diff --git a/x-pack/test/functional/apps/visualize/hybrid_visualization.ts b/x-pack/test/functional/apps/visualize/hybrid_visualization.ts index 03b6ed8e8e7c5..5ec6bd275133a 100644 --- a/x-pack/test/functional/apps/visualize/hybrid_visualization.ts +++ b/x-pack/test/functional/apps/visualize/hybrid_visualization.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); + const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'visChart']); const inspector = getService('inspector'); describe('hybrid index pattern', () => { @@ -81,7 +81,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.clickVisualizationByName('hybrid_histogram_line_chart'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); await inspector.open(); await inspector.setTablePageSize(50); await inspector.expectTableData(expectedData); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 17235c61c7d8c..664bfdf8d2a74 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -69,7 +69,7 @@ export default async function({ readConfigFile }) { esTestCluster: { license: 'trial', from: 'snapshot', - serverArgs: [], + serverArgs: ['path.repo=/tmp/'], }, kbnTestServer: { @@ -160,6 +160,10 @@ export default async function({ readConfigFile }) { ml: { pathname: '/app/ml', }, + roleMappings: { + pathname: '/app/kibana', + hash: '/management/security/role_mappings', + }, rollupJob: { pathname: '/app/kibana', hash: '/management/elasticsearch/rollup_jobs/', diff --git a/x-pack/test/functional/config_security_basic.js b/x-pack/test/functional/config_security_basic.js new file mode 100644 index 0000000000000..12d94e922a97c --- /dev/null +++ b/x-pack/test/functional/config_security_basic.js @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-default-export */ + +import { resolve } from 'path'; + +import { services } from './services'; +import { pageObjects } from './page_objects'; + +// the default export of config files must be a config provider +// that returns an object with the projects config values +export default async function({ readConfigFile }) { + const kibanaCommonConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../../../test/functional/config.js') + ); + + return { + // list paths to the files that contain your plugins tests + testFiles: [resolve(__dirname, './apps/security/basic_license')], + + services, + pageObjects, + + servers: kibanaFunctionalConfig.get('servers'), + + esTestCluster: { + license: 'basic', + from: 'snapshot', + serverArgs: [], + }, + + kbnTestServer: { + ...kibanaCommonConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), + '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', + '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions + '--telemetry.banner=false', + ], + }, + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + 'dateFormat:tz': 'UTC', + }, + }, + // the apps section defines the urls that + // `PageObjects.common.navigateTo(appKey)` will use. + // Merge urls for your plugin with the urls defined in + // Kibana's config in order to use this helper + apps: { + ...kibanaFunctionalConfig.get('apps'), + }, + + // choose where esArchiver should load archives from + esArchiver: { + directory: resolve(__dirname, 'es_archives'), + }, + + // choose where screenshots should be saved + screenshots: { + directory: resolve(__dirname, 'screenshots'), + }, + + junit: { + reportName: 'Chrome X-Pack UI Functional Tests', + }, + }; +} diff --git a/x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz b/x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz new file mode 100644 index 0000000000000..fda46096e1ab2 Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json b/x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json new file mode 100644 index 0000000000000..9544d05d70600 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json @@ -0,0 +1,104 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "endpoint-agent", + "mappings": { + "properties": { + "created_at": { + "type": "date" + }, + "endpoint": { + "properties": { + "active_directory_distinguished_name": { + "type": "text" + }, + "active_directory_hostname": { + "type": "text" + }, + "domain": { + "type": "text" + }, + "is_base_image": { + "type": "boolean" + }, + "isolation": { + "properties": { + "status": { + "type": "boolean" + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "sensor": { + "properties": { + "persistence": { + "type": "boolean" + }, + "status": { + "type": "object" + } + } + }, + "upgrade": { + "type": "object" + } + } + }, + "host": { + "properties": { + "hostname": { + "type": "text" + }, + "ip": { + "ignore_above": 256, + "type": "keyword" + }, + "mac_address": { + "type": "text" + }, + "name": { + "type": "text" + }, + "os": { + "properties": { + "full": { + "type": "text" + }, + "name": { + "type": "text" + } + } + } + } + }, + "machine_id": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/uptime/blank/mappings.json b/x-pack/test/functional/es_archives/uptime/blank/mappings.json index eb383cc87563d..a1b0696cdaadc 100644 --- a/x-pack/test/functional/es_archives/uptime/blank/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/blank/mappings.json @@ -1267,6 +1267,9 @@ "type": { "ignore_above": 1024, "type": "keyword" + }, + "timespan": { + "type": "date_range" } } }, diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 82011c48d4460..18ea515a73147 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -46,6 +46,7 @@ import { RemoteClustersPageProvider } from './remote_clusters_page'; import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_space_page'; import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; +import { RoleMappingsPageProvider } from './role_mappings_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -78,4 +79,5 @@ export const pageObjects = { remoteClusters: RemoteClustersPageProvider, copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, lens: LensPageProvider, + roleMappings: RoleMappingsPageProvider, }; diff --git a/x-pack/test/functional/page_objects/role_mappings_page.ts b/x-pack/test/functional/page_objects/role_mappings_page.ts new file mode 100644 index 0000000000000..b1adfb00af739 --- /dev/null +++ b/x-pack/test/functional/page_objects/role_mappings_page.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function RoleMappingsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async appTitleText() { + return await testSubjects.getVisibleText('appTitle'); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/snapshot_restore_page.ts b/x-pack/test/functional/page_objects/snapshot_restore_page.ts index 25bdfc7075727..1c8ba9f633111 100644 --- a/x-pack/test/functional/page_objects/snapshot_restore_page.ts +++ b/x-pack/test/functional/page_objects/snapshot_restore_page.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { FtrProviderContext } from '../ftr_provider_context'; export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { async appTitleText() { @@ -16,5 +16,50 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) async registerRepositoryButton() { return await testSubjects.find('registerRepositoryButton'); }, + async navToRepositories() { + await testSubjects.click('repositories_tab'); + await retry.waitForWithTimeout( + 'Wait for register repository button to be on page', + 10000, + async () => { + return await testSubjects.isDisplayed('registerRepositoryButton'); + } + ); + }, + async getRepoList() { + const table = await testSubjects.find('repositoryTable'); + const rows = await table.findAllByCssSelector('[data-test-subj="row"]'); + return await Promise.all( + rows.map(async row => { + return { + repoName: await ( + await row.findByCssSelector('[data-test-subj="Name_cell"]') + ).getVisibleText(), + repoLink: await ( + await row.findByCssSelector('[data-test-subj="Name_cell"]') + ).findByCssSelector('a'), + repoType: await ( + await row.findByCssSelector('[data-test-subj="Type_cell"]') + ).getVisibleText(), + repoEdit: await row.findByCssSelector('[data-test-subj="editRepositoryButton"]'), + repoDelete: await row.findByCssSelector('[data-test-subj="deleteRepositoryButton"]'), + }; + }) + ); + }, + async viewRepositoryDetails(name: string) { + const repos = await this.getRepoList(); + if (repos.length === 1) { + const repoToView = repos.filter(r => (r.repoName = name))[0]; + await repoToView.repoLink.click(); + } + await retry.waitForWithTimeout(`Repo title should be ${name}`, 10000, async () => { + return (await testSubjects.getVisibleText('title')) === name; + }); + }, + async performRepositoryCleanup() { + await testSubjects.click('cleanupRepositoryButton'); + return await testSubjects.getVisibleText('cleanupCodeBlock'); + }, }; } diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 26c95c3bf526d..f04f96148583f 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -70,5 +70,20 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo await uptimeService.setStatusFilterDown(); } } + + public async selectFilterItems(filters: Record) { + for (const key in filters) { + if (filters.hasOwnProperty(key)) { + const values = filters[key]; + for (let i = 0; i < values.length; i++) { + await uptimeService.selectFilterItem(key, values[i]); + } + } + } + } + + public async getSnapshotCount() { + return await uptimeService.getSnapshotCount(); + } })(); } diff --git a/x-pack/test/functional/services/uptime.ts b/x-pack/test/functional/services/uptime.ts index 40d2e3dafc7f8..ca38c2e9dd897 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -49,5 +49,20 @@ export function UptimeProvider({ getService }: FtrProviderContext) { async setStatusFilterDown() { await testSubjects.click('xpack.uptime.filterBar.filterStatusDown'); }, + async selectFilterItem(filterType: string, option: string) { + const popoverId = `filter-popover_${filterType}`; + const optionId = `filter-popover-item_${option}`; + await testSubjects.existOrFail(popoverId); + await testSubjects.click(popoverId); + await testSubjects.existOrFail(optionId); + await testSubjects.click(optionId); + await testSubjects.click(popoverId); + }, + async getSnapshotCount() { + return { + up: await testSubjects.getVisibleText('xpack.uptime.snapshot.donutChart.up'), + down: await testSubjects.getVisibleText('xpack.uptime.snapshot.donutChart.down'), + }; + }, }; } diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts index 630ec2792b9bf..1f5a64835416a 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts @@ -100,7 +100,7 @@ export default function({ getService }: FtrProviderContext) { }); // FLAKY: https://github.com/elastic/kibana/issues/43938 - it.skip('should succeed if both the OpenID Connect response and the cookie are provided', async () => { + it('should succeed if both the OpenID Connect response and the cookie are provided', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; diff --git a/x-pack/test/pki_api_integration/config.ts b/x-pack/test/pki_api_integration/config.ts index 50b41ad251827..a445b3d4943b0 100644 --- a/x-pack/test/pki_api_integration/config.ts +++ b/x-pack/test/pki_api_integration/config.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; // @ts-ignore -import { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { services } from './services'; export default async function({ readConfigFile }: FtrConfigProviderContext) { @@ -54,8 +54,8 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.key=${ES_KEY_PATH}`, - `--server.ssl.certificate=${ES_CERT_PATH}`, + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, `--server.ssl.certificateAuthorities=${JSON.stringify([ CA_CERT_PATH, resolve(__dirname, './fixtures/kibana_ca.crt'), diff --git a/x-pack/test/pki_api_integration/fixtures/README.md b/x-pack/test/pki_api_integration/fixtures/README.md index 0fcbc76183b48..ac2be482c6e33 100644 --- a/x-pack/test/pki_api_integration/fixtures/README.md +++ b/x-pack/test/pki_api_integration/fixtures/README.md @@ -1,7 +1,16 @@ # PKI Fixtures -* `es_ca.key` - the CA key used to sign certificates from @kbn/dev-utils that are used and trusted by test Elasticsearch server. -* `first_client.p12` and `second_client.p12` - the client certificate bundles signed by `es_ca.key` and hence trusted by -both test Kibana and Elasticsearch servers. +* `first_client.p12` and `second_client.p12` - the client certificate bundles signed by the Elastic Stack CA (in `kbn-dev-utils`) +and hence trusted by both test Kibana and Elasticsearch servers. * `untrusted_client.p12` - the client certificate bundle trusted by test Kibana server, but not test Elasticsearch test server. -* `kibana_ca.crt` and `kibana_ca.key` - the CA certificate and key trusted by test Kibana server only. \ No newline at end of file +* `kibana_ca.crt` and `kibana_ca.key` - the CA certificate and key trusted by test Kibana server only. + +The `first_client.p12` and `second_client.p12` files were generated the same time as the other certificates in `kbn-dev-utils`, using the +following commands: + +``` +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name first_client --pass "" +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name second_client --pass "" +``` + +If that CA is ever changed, these two files must be regenerated. diff --git a/x-pack/test/pki_api_integration/fixtures/es_ca.key b/x-pack/test/pki_api_integration/fixtures/es_ca.key deleted file mode 100644 index 5428f86851e5a..0000000000000 --- a/x-pack/test/pki_api_integration/fixtures/es_ca.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAjSJiqfwPZfvgHO1OZbxzgPn2EW/KewIHXygTAdL926Pm6R45 -G5H972B46NcSUoOZbOhDyvg6OKMJAICiXa85yOf3nyTo4APspR+K4AH60SEJohRF -mZwL/OryfiKvN5n5DxC2+Hb1wouwBUJM6DP62C24ve8YWuWwNkhJqWKe1YQUzPc1 -svqvU5uaHTzvLtp++RqSDNkcIqWl5S9Ip5PtOv6MHkCaIr2g4KQzplFwhT5qVd1Q -nYVBsQ0D8htLqUJBfjW0KHouEZpbjxJlc+EuyExS1o1+y3mVT+t2yZHAoIquh5ve -5A7a/RGJTyoR5u1DFs4Tcx2378kjA86gCQtClwIDAQABAoIBAFTOGKMzxrztQJmh -Lr6LIoyZpnaLygtoCK3xEprCAbB9KD9j3cTnUMMKIR0oPuY+FW8Pkczgo3ts2/fl -U6sfo4VJfc2vDA+vy/7cmUJJbkFDrNorfDb1QW7UbqnEhazPZIzc6lUahkpETZyb -XkMZGN3Ve3EFvojAA8ZaYYjarb52HRddLPZJ7c8ZiHfJ1jHNIvx6dIQ6CJVuovBJ -OGbbSAK8MjUtOI2XzWNHgUqGHcjVDFysuAac3ckK14TaN4KVNRl+usAMkZwqSM5u -j/ATFL9hx7nkzh3KWPsuOLMoLX7JN81z0YtT52wTxJoSiZKk/u91JHZ3NcrsOSPS -oLvVkyECgYEA16qtXvtmboAbqeuXf0nF+7QD0b+MdaRFIacqTG0LpEgY9Tjgs9Pn -6z44tHABWPVkRLNQZiky99MAq4Ci354Bk9dmylCw9ADH78VGmKWklbQEr1rw4dqm -DHTj9NQ79SyTdiasQjnnxCilWkrO6ZUqD8og4DT5MhzfxO/ZND8arGsCgYEAp4df -oI5lwlc1n9X/G9RQAKwNM5un8RmReleUVemjkcvWwvZVEjV0Gcc1WtjB+77Y5B9B -CM3laURDGrAgX5VS/I2jb0xqBNUr8XccSkDQAP9UuVPZgxpS+8d0R3fxVzniHWwR -WC2dW/Is40i/6+7AkFXhkiFiqxkvSg4pWHPazYUCgYB/gP7C+urSRZcVXJ3SuXD9 -oK3pYc/O9XGRtd0CFi4d0CpBQIFIj+27XKv1sYp6Z4oCO+k6nPzvG6Z3vrOMdUQF -fgHddttHRvbtwLo+ISAvCaEDc0aaoMQu9SSYaKmSB+qenbqV5NorVMR9n2C5JGEb -uKq7I1Z41C1Pp2XIx84jRQKBgQCjKvfZsjesZDJnfg9dtJlDPlARXt7gte16gkiI -sOnOfAGtnCzZclSlMuBlnk65enVXIpW+FIQH1iOhn7+4OQE92FpBceSk1ldZdJCK -RbwR7J5Bb0igJ4iBkA9R+KGIOmlgDLyL7MmiHyrXKCk9iynkqrDsGjY2vW3QrCBa -9WQ73QKBgQDAYZzplO4TPoPK9AnxoW/HpSwGEO7Fb8fLyPg94CvHn4QBCFJUKuTn -hBp/TJgF6CjQWQMr2FKVFF33Ow7+Qa96YGvmYlEjR/71D4Rlprj5JJpuO154DI3I -YIMNTjvwEQEI+YamMarKsz0Kq+I1EYSAf6bQ4H2PgxDxwTXaLkl0RA== ------END RSA PRIVATE KEY----- diff --git a/x-pack/test/pki_api_integration/fixtures/first_client.p12 b/x-pack/test/pki_api_integration/fixtures/first_client.p12 index 62da80d9ab80e..9d838199e8392 100644 Binary files a/x-pack/test/pki_api_integration/fixtures/first_client.p12 and b/x-pack/test/pki_api_integration/fixtures/first_client.p12 differ diff --git a/x-pack/test/pki_api_integration/fixtures/second_client.p12 b/x-pack/test/pki_api_integration/fixtures/second_client.p12 index 1c85686cb7b68..f41c0e030ba79 100644 Binary files a/x-pack/test/pki_api_integration/fixtures/second_client.p12 and b/x-pack/test/pki_api_integration/fixtures/second_client.p12 differ diff --git a/x-pack/test/reporting/README.md b/x-pack/test/reporting/README.md index 30859fa96c015..d4a6c20a835e2 100644 --- a/x-pack/test/reporting/README.md +++ b/x-pack/test/reporting/README.md @@ -82,16 +82,6 @@ node scripts/functional_tests_server.js --config test/reporting/configs/chromium **Note:** Dashboard has some snapshot testing too, in `_dashboard_snapshots.js`. This test watches for a command line flag `--updateBaselines` which automates updating the baselines. Probably worthwhile to do some similar here in the long run. - ### Adding a new BWC test - - We have tests that ensure the latest version of Kibana will continue to generate reports from URLs generated in previous versions, to ensure backward compatibility. These tests are in `api/bwc_generation_urls.js`. It's important to update these every now and then and add new ones, especially if anything in the URL changed in a release. - - To add test coverage for a specific minor release,: -1. Checkout previous branch, e.g. `git checkout upstream/6.4` -2. Sync your environment via `yarn kbn bootstrap` (Note, if you run into problems you may want to first clean via `yarn kbn clean`) -3. Start up kibana and Elasticsearch (`yarn es snapshot --license trial` in one terminal, and `yarn start` in another) -4. Load the reporting test data that is used in the tests. Ensure you are in the `x-pack` directory and run: - ``` node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana ``` diff --git a/x-pack/test/reporting/api/bwc_existing_indexes.js b/x-pack/test/reporting/api/bwc_existing_indexes.js deleted file mode 100644 index ffcf123848bb2..0000000000000 --- a/x-pack/test/reporting/api/bwc_existing_indexes.js +++ /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 * as GenerationUrls from './generation_urls'; - -/** - * This file tests the situation when a reporting index spans releases. By default reporting indexes are created - * on a weekly basis, but this is configurable so it is possible a user has this set to yearly. In that event, it - * is possible report data is getting posted to an index that was created by a very old version. We don't have a - * reporting index migration plan, so this test is important to ensure BWC, or that in the event we decide to make - * a major change in a major release, we handle it properly. - */ - -export default function({ getService }) { - const esArchiver = getService('esArchiver'); - const reportingAPI = getService('reportingAPI'); - const usageAPI = getService('usageAPI'); - - // FLAKY: https://github.com/elastic/kibana/issues/42725 - describe.skip('BWC report generation into existing indexes', () => { - let expectedCompletedReportCount; - let cleanupIndexAlias; - - describe('existing 6_2 index', () => { - before('load data and add index alias', async () => { - await reportingAPI.deleteAllReportingIndexes(); - await esArchiver.load('reporting/bwc/6_2'); - - // The index name in the 6_2 archive. - const ARCHIVED_REPORTING_INDEX = '.reporting-2018.03.11'; - cleanupIndexAlias = await reportingAPI.coerceReportsIntoExistingIndex( - ARCHIVED_REPORTING_INDEX - ); - - const stats = await usageAPI.getUsageStats(); - expectedCompletedReportCount = await reportingAPI.getCompletedReportCount(stats); - - await esArchiver.unload('reporting/bwc/6_2'); - }); - - after('remove index alias', async () => { - await cleanupIndexAlias(); - }); - - // Might not be great test practice to lump all these jobs together but reporting takes awhile and it'll be - // more efficient to post them all up front, then sequentially. - it('multiple jobs posted', async () => { - const reportPaths = []; - reportPaths.push( - await reportingAPI.postJob(GenerationUrls.CSV_DISCOVER_KUERY_AND_FILTER_6_3) - ); - reportPaths.push( - await reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_DASHBOARD_FILTER_6_3) - ); - reportPaths.push( - await reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_PIE_VISUALIZATION_6_3) - ); - reportPaths.push(await reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_3)); - reportPaths.push( - await reportingAPI.postJob( - GenerationUrls.PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 - ) - ); - - await reportingAPI.expectAllJobsToFinishSuccessfully(reportPaths); - }).timeout(1540000); - - it('jobs completed successfully', async () => { - const stats = await usageAPI.getUsageStats(); - expectedCompletedReportCount += 5; - reportingAPI.expectCompletedReportCount(stats, expectedCompletedReportCount); - }); - }); - }); -} diff --git a/x-pack/test/reporting/api/bwc_generation_urls.js b/x-pack/test/reporting/api/bwc_generation_urls.js deleted file mode 100644 index 25b40ff3f74a8..0000000000000 --- a/x-pack/test/reporting/api/bwc_generation_urls.js +++ /dev/null @@ -1,54 +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 GenerationUrls from './generation_urls'; - -export default function({ getService }) { - const reportingAPI = getService('reportingAPI'); - const usageAPI = getService('usageAPI'); - - describe('BWC report generation urls', () => { - describe('Pre 6_2', () => { - before(async () => { - await reportingAPI.deleteAllReportingIndexes(); - }); - - // The URL being tested was captured from release 6.4 and then the layout section was removed to test structure before - // preserve_layout was introduced. See https://github.com/elastic/kibana/issues/23414 - it('job posted successfully', async () => { - const path = await reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_PRE_6_2); - await reportingAPI.waitForJobToFinish(path); - const stats = await usageAPI.getUsageStats(); - reportingAPI.expectCompletedReportCount(stats, 1); - }).timeout(500000); - }); - - describe('6_2', () => { - before(async () => { - await reportingAPI.deleteAllReportingIndexes(); - }); - - // Might not be great test practice to lump all these jobs together but reporting takes awhile and it'll be - // more efficient to post them all up front, then sequentially. - it('multiple jobs posted', async () => { - const reportPaths = []; - reportPaths.push(await reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_2)); - reportPaths.push(await reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_VISUALIZATION_6_2)); - reportPaths.push(await reportingAPI.postJob(GenerationUrls.CSV_DISCOVER_FILTER_QUERY_6_2)); - - await reportingAPI.expectAllJobsToFinishSuccessfully(reportPaths); - }).timeout(1540000); - - it('jobs completed successfully', async () => { - const stats = await usageAPI.getUsageStats(); - reportingAPI.expectCompletedReportCount(stats, 3); - }); - }); - - // 6.3 urls currently being tested as part of the "bwc_existing_indexes" test suite. Reports are time consuming, - // don't replicate tests if we don't need to, so no specific 6_3 url tests here. - }); -} diff --git a/x-pack/test/reporting/api/chromium_tests.js b/x-pack/test/reporting/api/chromium_tests.js index 6594a80db491b..2d5a31bb40da3 100644 --- a/x-pack/test/reporting/api/chromium_tests.js +++ b/x-pack/test/reporting/api/chromium_tests.js @@ -27,8 +27,6 @@ export default function({ loadTestFile, getService }) { await esArchiver.unload(OSS_DATA_ARCHIVE_PATH); }); - loadTestFile(require.resolve('./bwc_existing_indexes')); - loadTestFile(require.resolve('./bwc_generation_urls')); loadTestFile(require.resolve('./usage')); }); } diff --git a/x-pack/test/reporting/api/generation_urls.js b/x-pack/test/reporting/api/generation_urls.js index 182d395704a25..b98b1a26651a1 100644 --- a/x-pack/test/reporting/api/generation_urls.js +++ b/x-pack/test/reporting/api/generation_urls.js @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// These all have the domain name portion stripped out. The api infrastructure assumes it when we post to it anyhow. - -// The URL below was captured from release 6.4 and then the layout section was removed to test structure before -// preserve_layout was introduced. See https://github.com/elastic/kibana/issues/23414 -export const PDF_PRINT_DASHBOARD_PRE_6_2 = - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,objectType:dashboard,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fdashboard%2F2ae34a60-3dd4-11e8-b2b9-5d5dc1715159%3F_g%3D(refreshInterval:(pause:!!t,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(),gridData:(h:15,i:!%271!%27,w:24,x:0,y:0),id:!%27145ced90-3dcb-11e8-8660-4d65aa086b3c!%27,panelIndex:!%271!%27,type:visualization,version:!%276.3.0!%27),(embeddableConfig:(),gridData:(h:15,i:!%272!%27,w:24,x:24,y:0),id:e2023110-3dcb-11e8-8660-4d65aa086b3c,panelIndex:!%272!%27,type:visualization,version:!%276.3.0!%27)),query:(language:lucene,query:!%27!%27),timeRestore:!!f,title:!%27couple%2Bpanels!%27,viewMode:view)%27),title:%27couple%20panels%27)'; - export const PDF_PRINT_DASHBOARD_6_3 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:dashboard,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fdashboard%2F2ae34a60-3dd4-11e8-b2b9-5d5dc1715159%3F_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(),gridData:(h:15,i:!%271!%27,w:24,x:0,y:0),id:!%27145ced90-3dcb-11e8-8660-4d65aa086b3c!%27,panelIndex:!%271!%27,type:visualization,version:!%276.3.0!%27),(embeddableConfig:(),gridData:(h:15,i:!%272!%27,w:24,x:24,y:0),id:e2023110-3dcb-11e8-8660-4d65aa086b3c,panelIndex:!%272!%27,type:visualization,version:!%276.3.0!%27)),query:(language:lucene,query:!%27!%27),timeRestore:!!f,title:!%27couple%2Bpanels!%27,viewMode:view)%27),title:%27couple%20panels%27)'; export const PDF_PRESERVE_DASHBOARD_FILTER_6_3 = @@ -21,10 +14,3 @@ export const PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:visualization,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fvisualize%2Fedit%2Fbefdb6b0-3e59-11e8-9fc3-39e49624228e%3F_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(filters:!!((!%27$state!%27:(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!%27!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!%271!%27,otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!%27Filter%2BTest:%2Banimals:%2Blinked%2Bto%2Bsearch%2Bwith%2Bfilter!%27,type:pie))%27),title:%27Filter%20Test:%20animals:%20linked%20to%20search%20with%20filter%27)'; export const CSV_DISCOVER_KUERY_AND_FILTER_6_3 = '/api/reporting/generate/csv?jobParams=(conflictedTypesFields:!(),fields:!(%27@timestamp%27,agent,bytes,clientip),indexPatternId:%270bf35f60-3dc9-11e8-8660-4d65aa086b3c%27,metaFields:!(_source,_id,_type,_index,_score),searchRequest:(body:(_source:(excludes:!(),includes:!(%27@timestamp%27,agent,bytes,clientip)),docvalue_fields:!(%27@timestamp%27),query:(bool:(filter:!((bool:(minimum_should_match:1,should:!((match:(clientip:%2773.14.212.83%27)))))),must:!((range:(bytes:(gte:100,lt:1000))),(range:(%27@timestamp%27:(format:epoch_millis,gte:1369165215770,lte:1526931615770)))),must_not:!(),should:!())),script_fields:(),sort:!((%27@timestamp%27:(order:desc,unmapped_type:boolean))),stored_fields:!(%27@timestamp%27,agent,bytes,clientip),version:!t),index:%27logstash-*%27),title:%27Bytes%20and%20kuery%20in%20saved%20search%20with%20filter%27,type:search)'; - -export const PDF_PRINT_DASHBOARD_6_2 = - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:dashboard,queryString:%27_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!((!%27$state!%27:(store:appState),meta:(alias:!!n,disabled:!!f,field:isDog,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:isDog,negate:!!f,params:(value:!!t),type:phrase,value:true),script:(script:(inline:!%27boolean%2Bcompare(Supplier%2Bs,%2Bdef%2Bv)%2B%257Breturn%2Bs.get()%2B%253D%253D%2Bv%3B%257Dcompare(()%2B-%253E%2B%257B%2Breturn%2Bdoc%255B!!!%27animal.keyword!!!%27%255D.value%2B%253D%253D%2B!!!%27dog!!!%27%2B%257D,%2Bparams.value)%3B!%27,lang:painless,params:(value:!!t))))),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((gridData:(h:3,i:!%274!%27,w:6,x:6,y:0),id:edb65990-53ca-11e8-b481-c9426d020fcd,panelIndex:!%274!%27,type:visualization,version:!%276.2.4!%27),(gridData:(h:3,i:!%275!%27,w:6,x:0,y:0),id:!%270644f890-53cb-11e8-b481-c9426d020fcd!%27,panelIndex:!%275!%27,type:visualization,version:!%276.2.4!%27)),query:(language:lucene,query:!%27weightLbs:%253E15!%27),timeRestore:!!t,title:!%27Animal%2BWeights%2B(created%2Bin%2B6.2)!%27,viewMode:view)%27,savedObjectId:%271b2f47b0-53cb-11e8-b481-c9426d020fcd%27)'; -export const PDF_PRESERVE_VISUALIZATION_6_2 = - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(dimensions:(height:441,width:1002),id:preserve_layout),objectType:visualization,queryString:%27_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(filters:!!(),linked:!!f,query:(language:lucene,query:!%27weightLbs:%253E10!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(field:weightLbs,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!%271!%27,otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!%27Weight%2Bin%2Blbs%2Bpie%2Bcreated%2Bin%2B6.2!%27,type:pie))%27,savedObjectId:%270644f890-53cb-11e8-b481-c9426d020fcd%27)'; -export const CSV_DISCOVER_FILTER_QUERY_6_2 = - '/api/reporting/generate/csv?jobParams=(conflictedTypesFields:!(),fields:!(%27@timestamp%27,animal,sound,weightLbs),indexPatternId:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,metaFields:!(_source,_id,_type,_index,_score),searchRequest:(body:(_source:(excludes:!(),includes:!(%27@timestamp%27,animal,sound,weightLbs)),docvalue_fields:!(%27@timestamp%27),query:(bool:(filter:!(),must:!((query_string:(analyze_wildcard:!t,default_field:%27*%27,query:%27weightLbs:%3E10%27)),(match_phrase:(sound.keyword:(query:growl))),(range:(%27@timestamp%27:(format:epoch_millis,gte:1523310968000,lte:1523483768000)))),must_not:!(),should:!())),script_fields:(),sort:!((%27@timestamp%27:(order:desc,unmapped_type:boolean))),stored_fields:!(%27@timestamp%27,animal,sound,weightLbs),version:!t),index:%27animals-*%27),title:%27Search%20created%20in%206.2%27,type:search)'; diff --git a/x-pack/test/reporting/configs/chromium_api.js b/x-pack/test/reporting/configs/chromium_api.js index f016738c0e052..95649dfb5d7a3 100644 --- a/x-pack/test/reporting/configs/chromium_api.js +++ b/x-pack/test/reporting/configs/chromium_api.js @@ -28,6 +28,7 @@ export default async function({ readConfigFile }) { '["info","warning","error","fatal","optimize","reporting"]', '--xpack.endpoint.enabled=true', '--xpack.reporting.csv.enablePanelActionDownload=true', + '--xpack.reporting.capture.maxAttempts=1', '--xpack.security.session.idleTimeout=3600000', '--xpack.spaces.enabled=false', ], diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 7d72db8c08e22..8458f4e537b70 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { 'header', 'discover', 'visualize', + 'visEditor', ]); const log = getService('log'); @@ -298,9 +299,9 @@ export default function({ getService, getPageObjects }) { it('becomes available when saved', async () => { await PageObjects.reporting.setTimepickerInDataRange(); - await PageObjects.visualize.clickBucket('X-axis'); - await PageObjects.visualize.selectAggregation('Date Histogram'); - await PageObjects.visualize.clickGo(); + await PageObjects.visEditor.clickBucket('X-axis'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.clickGo(); await PageObjects.visualize.saveVisualization('my viz'); await PageObjects.reporting.openPdfReportingPanel(); await expectEnabledGenerateReportButton(); diff --git a/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml b/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml index 132faffd41667..a890fe812987b 100644 --- a/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml +++ b/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml @@ -6,25 +6,25 @@ - - MIIDMDCCAhgCCQCkOD7fnHiQrTANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB - VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 - cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwIBcNMTYwMTE5MjExNjQ5WhgP - MjA4NDAyMDYyMTE2NDlaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 - YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMT - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALR63SnR - LW/Dgl0Vuy8gB6KjWwvajRWeXNgvlf6LX56oNsUFREHBLQlC2uT5R26F3tOqCDbs - MFNoyDjMBinXRRFJJ2Sokue7GSsGvBv41LMTHnO/MeCCbEOqghJS/QI89cV+u+Aw - 9U+v426KAlCa1sGuE2+3/JvqdBQyheiukmGLiJ0OofpfgpYuFmKi2uYBKU3qzjUx - D01wQ4rCpq5nEnksGhgBeBDnheYmmDsj/wDvnz1exK/WprvTiHQ5MwuIQ4OybwgV - WDF+zv8PXrObrsZvD/ulrjh1cakvnCe2kDYEKMRiHUDesHS2jNJkBUe+FJo4/E3U - pFoYOtdoBe69BIUCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAQhjF91G0R662XJJ7 - jGMudA9VbRVCow8s68I/GWPZmpKxPAxwz0xiv1eFIoiP416LX9amnx3yAmUoN4Wr - Cq0jsgyT1AOiSCdxkvYsqQG3SFVVt5BDLjThH66Vxi7Bach6SyATa1NG588mg7n9 - pPJ4A1rcj+5kZuwnd52kfVLP+535lylwMyoyJa2AskieRPLNSzus2eUDTR6F+9Mb - eLOwp5rMl2nNYfLXUCSqEeC6uPu0yq6Tu0N0SjThfKndd2NU1fk3zyOjxyCIhGPe - G8VhrPY4lkJ9EE9Tuq095jwd1+q9fYzlKZWhOmg+IcOwUMgbgeWpeZTAhUIZAnia - 4UH6NA== + + MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== diff --git a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts index f8862e6fb209d..b7b94b8eeb17a 100644 --- a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts +++ b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts @@ -12,6 +12,7 @@ import zlib from 'zlib'; import { promisify } from 'util'; import { parseString } from 'xml2js'; import { SignedXml } from 'xml-crypto'; +import { KBN_KEY_PATH } from '@kbn/dev-utils'; /** * @file Defines a set of tools that allow us to parse and generate various SAML XML messages. @@ -24,7 +25,7 @@ const inflateRawAsync = promisify(zlib.inflateRaw); const deflateRawAsync = promisify(zlib.deflateRaw); const parseStringAsync = promisify(parseString); -const signingKey = fs.readFileSync(require.resolve('../../../../test/dev_certs/server.key')); +const signingKey = fs.readFileSync(KBN_KEY_PATH); const signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; export async function getSAMLRequestId(urlWithSAMLRequestId: string) { diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 7383bb3409f1a..00ab75702e90d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -87,14 +87,8 @@ export function copyToSpaceTestSuiteFactory( body: { size: 0, query: { - bool: { - must_not: { - term: { - // exclude spaces from the result set. - // we don't assert on these. - type: 'space', - }, - }, + terms: { + type: ['visualization', 'dashboard', 'index-pattern'], }, }, aggs: { diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index e3994634be1d9..9036fcbf7a8dd 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -39,6 +39,11 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe index: '.kibana', body: { size: 0, + query: { + terms: { + type: ['visualization', 'dashboard', 'space', 'config', 'index-pattern'], + }, + }, aggs: { count: { terms: { diff --git a/x-pack/test/typings/encode_uri_query.d.ts b/x-pack/test/typings/encode_uri_query.d.ts deleted file mode 100644 index e1ab5f4a70abf..0000000000000 --- a/x-pack/test/typings/encode_uri_query.d.ts +++ /dev/null @@ -1,11 +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. - */ - -declare module 'encode-uri-query' { - function encodeUriQuery(query: string, usePercentageSpace?: boolean): string; - // eslint-disable-next-line import/no-default-export - export default encodeUriQuery; -} diff --git a/x-pack/test_utils/stub_web_worker.ts b/x-pack/test_utils/stub_web_worker.ts new file mode 100644 index 0000000000000..2e7d5cf2098c8 --- /dev/null +++ b/x-pack/test_utils/stub_web_worker.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +if (!window.Worker) { + // @ts-ignore we aren't honoring the real Worker spec here + window.Worker = function Worker() { + this.postMessage = jest.fn(); + + // @ts-ignore TypeScript doesn't think this exists on the Worker interface + // https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate + this.terminate = jest.fn(); + }; +} diff --git a/x-pack/typings/encode_uri_query.d.ts b/x-pack/typings/encode_uri_query.d.ts deleted file mode 100644 index e1ab5f4a70abf..0000000000000 --- a/x-pack/typings/encode_uri_query.d.ts +++ /dev/null @@ -1,11 +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. - */ - -declare module 'encode-uri-query' { - function encodeUriQuery(query: string, usePercentageSpace?: boolean): string; - // eslint-disable-next-line import/no-default-export - export default encodeUriQuery; -} diff --git a/yarn.lock b/yarn.lock index 0026370927fe1..96bb533120aa7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1361,10 +1361,10 @@ dependencies: "@elastic/apm-rum-core" "^4.7.0" -"@elastic/charts@^16.0.2": - version "16.0.2" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-16.0.2.tgz#35068a08a19534da62e9bcad700cc7b2a15bc55a" - integrity sha512-0tVyltAmAPOAfiRU1iKYk3b9++4oTn6IXvyM4SSj7Ukh5Y90XXmOtGEUPnZTiRPmup9MJi4srrm9ra9k/Kq4UQ== +"@elastic/charts@^16.1.0": + version "16.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-16.1.0.tgz#67cf11625dcd7e1c2cf16ef53349e6a68a73f5b1" + integrity sha512-0jZ7thhGmYC0ZdEVkxfg6M66epCD7k7BfYIi12FnrmIK+mUD2IPhR8b2TJXvaojPryN4YTNreGRncQ9R58fOoQ== dependencies: "@types/d3-shape" "^1.3.1" classnames "^2.2.6" @@ -3462,10 +3462,10 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/eslint@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-6.1.2.tgz#297ece0f3815f93d699b18bdade5e6bee747284f" - integrity sha512-t+smTKg1e9SshiIOI94Zi+Lvo3bfHF20MuKP8w3VGWdrS1dYm33A7xrSoyy9FQv6oE2TwYqEXVJ50I0or8+FWQ== +"@types/eslint@^6.1.3": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-6.1.3.tgz#ec2a66e445a48efaa234020eb3b6e8f06afc9c61" + integrity sha512-llYf1QNZaDweXtA7uY6JczcwHmFwJL9TpK3E6sY0B18l6ulDT6VWNMAdEjYccFHiDfxLPxffd8QmSDV4QUUspA== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -3882,6 +3882,13 @@ dependencies: "@types/node" "*" +"@types/node-forge@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.0.tgz#e9f678ec09283f9f35cb8de6c01f86be9278ac08" + integrity sha512-J00+BIHJOfagO1Qs67Jp5CZO3VkFxY8YKMt44oBhXr+3ZYNnl8wv/vtcJyPjuH0QZ+q7+5nnc6o/YH91ZJy2pQ== + dependencies: + "@types/node" "*" + "@types/node-jose@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/node-jose/-/node-jose-1.1.0.tgz#26e1d234b41a39035482443ef35414bf34ba5d8b" @@ -3944,6 +3951,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a" integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA== +"@types/pegjs@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94" + integrity sha512-ra8IchO9odGQmYKbm+94K58UyKCEKdZh9y0vxhG4pIpOJOBlC1C+ZtBVr6jLs+/oJ4pl+1p/4t3JtBA8J10Vvw== + "@types/pngjs@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4" @@ -4003,10 +4015,10 @@ dependencies: "@types/react" "*" -"@types/react-beautiful-dnd@^11.0.3": - version "11.0.3" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#51d9f37942dd18cc4aa10da98a5c883664e7ee46" - integrity sha512-7ZbT/7mNJu+uRrUGdTQ1hAINtqg909L4NHrXyspV42fvVgBgda6ysiBzoDUMENmQ/RlRJdpyrcp8Dtd/77bp9Q== +"@types/react-beautiful-dnd@^11.0.4": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-11.0.4.tgz#25cdf16864df8fd1d82f9416c8c0fd957e793024" + integrity sha512-a1Nvt1AcSEA962OuXrk1gu5bJQhzu0B3qFNO999/0nmF+oAD7HIAY0DwraS3L3XM1cVuRO1+PtpTkD4CfRK2QA== dependencies: "@types/react" "*" @@ -4148,10 +4160,10 @@ dependencies: redux "^4.0.0" -"@types/redux-actions@^2.2.1": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.3.0.tgz#d28d7913ec86ee9e20ecb33a1fed887ecb538149" - integrity sha512-N5gZT7Tg5HGRbQH56D6umLhv1R4koEFjfz5+2TFo/tjAz3Y3Aj+hjQBum3UUO4D53hYO439UlWP5Q+S63vujrQ== +"@types/redux-actions@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.1.tgz#0940e97fa35ad3004316bddb391d8e01d2efa605" + integrity sha512-zKgK+ATp3sswXs6sOYo1tk8xdXTy4CTaeeYrVQlClCjeOpag5vzPo0ASWiiBJ7vsiQRAdb3VkuFLnDoBimF67g== "@types/redux@^3.6.31": version "3.6.31" @@ -4434,57 +4446,40 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.12.0": - version "2.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.12.0.tgz#0da7cbca7b24f4c6919e9eb31c704bfb126f90ad" - integrity sha512-1t4r9rpLuEwl3hgt90jY18wJHSyb0E3orVL3DaqwmpiSDHmHiSspVsvsFF78BJ/3NNG3qmeso836jpuBWYziAA== +"@typescript-eslint/eslint-plugin@^2.15.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.15.0.tgz#5442c30b687ffd576ff74cfea46a6d7bfb0ee893" + integrity sha512-XRJFznI5v4K1WvIrWmjFjBAdQWaUTz4xJEdqR7+wAFsv6Q9dP3mOlE6BMNT3pdlp9eF1+bC5m5LZTmLMqffCVw== dependencies: - "@typescript-eslint/experimental-utils" "2.12.0" + "@typescript-eslint/experimental-utils" "2.15.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.12.0": - version "2.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.12.0.tgz#e0a76ffb6293e058748408a191921e453c31d40d" - integrity sha512-jv4gYpw5N5BrWF3ntROvCuLe1IjRenLy5+U57J24NbPGwZFAjhnM45qpq0nDH1y/AZMb3Br25YiNVwyPbz6RkA== +"@typescript-eslint/experimental-utils@2.15.0", "@typescript-eslint/experimental-utils@^2.5.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.15.0.tgz#41e35313bfaef91650ddb5380846d1c78a780070" + integrity sha512-Qkxu5zndY5hqlcQkmA88gfLvqQulMpX/TN91XC7OuXsRf4XG5xLGie0sbpX97o/oeccjeZYRMipIsjKk/tjDHA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.12.0" + "@typescript-eslint/typescript-estree" "2.15.0" eslint-scope "^5.0.0" -"@typescript-eslint/experimental-utils@^1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz#b08c60d780c0067de2fb44b04b432f540138301e" - integrity sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "1.13.0" - eslint-scope "^4.0.0" - -"@typescript-eslint/parser@^2.12.0": - version "2.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.12.0.tgz#393f1604943a4ca570bb1a45bc8834e9b9158884" - integrity sha512-lPdkwpdzxEfjI8TyTzZqPatkrswLSVu4bqUgnB03fHSOwpC7KSerPgJRgIAf11UGNf7HKjJV6oaPZI4AghLU6g== +"@typescript-eslint/parser@^2.15.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.15.0.tgz#379a71a51b0429bc3bc55c5f8aab831bf607e411" + integrity sha512-6iSgQsqAYTaHw59t0tdjzZJluRAjswdGltzKEdLtcJOxR2UVTPHYvZRqkAVGCkaMVb6Fpa60NnuozNCvsSpA9g== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.12.0" - "@typescript-eslint/typescript-estree" "2.12.0" + "@typescript-eslint/experimental-utils" "2.15.0" + "@typescript-eslint/typescript-estree" "2.15.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz#8140f17d0f60c03619798f1d628b8434913dc32e" - integrity sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw== - dependencies: - lodash.unescape "4.0.1" - semver "5.5.0" - -"@typescript-eslint/typescript-estree@2.12.0": - version "2.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.12.0.tgz#bd9e547ccffd17dfab0c3ab0947c80c8e2eb914c" - integrity sha512-rGehVfjHEn8Frh9UW02ZZIfJs6SIIxIu/K1bbci8rFfDE/1lQ8krIJy5OXOV3DVnNdDPtoiPOdEANkLMrwXbiQ== +"@typescript-eslint/typescript-estree@2.15.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.15.0.tgz#79ae52eed8701b164d91e968a65d85a9105e76d3" + integrity sha512-L6Pog+w3VZzXkAdyqA0VlwybF8WcwZX+mufso86CMxSdWmcizJ38lgBdpqTbc9bo92iyi0rOvmATKiwl+amjxg== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -4764,11 +4759,6 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - acorn-globals@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" @@ -4799,7 +4789,7 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn-jsx@^5.0.2: +acorn-jsx@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== @@ -4843,7 +4833,7 @@ acorn@^6.0.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== -acorn@^6.0.5, acorn@^6.2.1: +acorn@^6.2.1: version "6.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== @@ -7989,7 +7979,7 @@ chroma-js@^1.4.1: resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-1.4.1.tgz#eb2d9c4d1ff24616be84b35119f4d26f8205f134" integrity sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ== -chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: +chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== @@ -10968,11 +10958,6 @@ enabled@1.0.x: dependencies: env-variable "0.0.x" -encode-uri-query@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encode-uri-query/-/encode-uri-query-1.0.1.tgz#e9c70d3e1aab71b039e55b38a166013508803ba8" - integrity sha1-6ccNPhqrcbA55Vs4oWYBNQiAO6g= - encodeurl@^1.0.2, encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -11432,10 +11417,10 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-config-prettier@^6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.4.0.tgz#0a04f147e31d33c6c161b2dd0971418ac52d0477" - integrity sha512-YrKucoFdc7SEko5Sxe4r6ixqXPDP1tunGw91POeZTTRKItf/AMFYt/YLEQtZMkR2LVpAVhcAcZgcWpm1oGPW7w== +eslint-config-prettier@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.9.0.tgz#430d24822e82f7deb1e22a435bfa3999fae4ad64" + integrity sha512-k4E14HBtcLv0uqThaI6I/n1LEqROp8XaPu6SO9Z32u5NlGRC07Enu1Bh2KEFw4FNHbekH8yzbIU9kUGxbiGmCA== dependencies: get-stdin "^6.0.0" @@ -11474,20 +11459,12 @@ eslint-import-resolver-webpack@0.11.1: resolve "^1.10.0" semver "^5.3.0" -eslint-module-utils@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c" - integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw== - dependencies: - debug "^2.6.8" - pkg-dir "^2.0.0" - -eslint-module-utils@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a" - integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw== +eslint-module-utils@2.5.0, eslint-module-utils@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.5.0.tgz#cdf0b40d623032274ccd2abd7e64c4e524d6e19c" + integrity sha512-kCo8pZaNz2dsAW7nCUjuVoI11EBXXpIzfNxmaoLhXoRDOnqXLC4iSGVRdZPhOitfbdEfMEfKOiENaK6wDPZEGw== dependencies: - debug "^2.6.8" + debug "^2.6.9" pkg-dir "^2.0.0" eslint-plugin-babel@^5.3.0: @@ -11497,51 +11474,57 @@ eslint-plugin-babel@^5.3.0: dependencies: eslint-rule-composer "^0.3.0" -eslint-plugin-ban@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-ban/-/eslint-plugin-ban-1.3.0.tgz#be9714cb9e01a1adec6c86cdb182e97636eafe44" - integrity sha512-A9A2z60UeVj7/BdKzeIjVEGAog/4QXAyOkZ98AUnZc7fsRp+J7YW7+U/YEVpBJqjSiU/FGUA5tGJoI34ul/TyA== +eslint-plugin-ban@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-ban/-/eslint-plugin-ban-1.4.0.tgz#b3a7b000412921336b1feeece5b8ce9a69dea605" + integrity sha512-wtrUOLg8WUiGDkVnmyMseLRtXYBM+bJTe2STvhqznHVj6RPAiNEVLbvDj2b0WWwY/2ldKqeaw3iHUHwfCJ8c8Q== dependencies: requireindex "~1.2.0" -eslint-plugin-cypress@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.7.0.tgz#117f14ce63698e4c4f3afea3d7e27025c8d504f0" - integrity sha512-52Lq5ePCD/8jc536e1RqtLfj33BAy1s7BlYgCjbG39J5kqUitcTlRY5i3NRoeAyPHueDwETsq0eASF44ugLosQ== +eslint-plugin-cypress@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.8.1.tgz#981a0f3658b40de430bcf05cabc96b396487c91f" + integrity sha512-jDpcP+MmjmqQO/x3bwIXgp4cl7Q66RYS5/IsuOQP4Qo2sEqE3DI8tTxBQ1EhnV5qEDd2Z2TYHR+5vYI6oCN4uw== dependencies: globals "^11.12.0" -eslint-plugin-es@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" - integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== +eslint-plugin-es@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.0.tgz#98cb1bc8ab0aa807977855e11ad9d1c9422d014b" + integrity sha512-6/Jb/J/ZvSebydwbBJO1R9E5ky7YeElfK56Veh7e4QGFHCXoIXGH9HhVz+ibJLM3XJ1XjP+T7rKBLUa/Y7eIng== dependencies: - eslint-utils "^1.4.2" + eslint-utils "^2.0.0" regexpp "^3.0.0" -eslint-plugin-import@^2.18.2: - version "2.18.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" - integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== +eslint-plugin-eslint-plugin@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz#a7a00f15a886957d855feacaafee264f039e62d5" + integrity sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg== + +eslint-plugin-import@^2.19.1: + version "2.19.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.19.1.tgz#5654e10b7839d064dd0d46cd1b88ec2133a11448" + integrity sha512-x68131aKoCZlCae7rDXKSAQmbT5DQuManyXo2sK6fJJ0aK5CWAkv6A6HJZGgqC8IhjQxYPgo6/IY4Oz8AFsbBw== dependencies: array-includes "^3.0.3" + array.prototype.flat "^1.2.1" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.0" + eslint-module-utils "^2.4.1" has "^1.0.3" minimatch "^3.0.4" object.values "^1.1.0" read-pkg-up "^2.0.0" - resolve "^1.11.0" + resolve "^1.12.0" -eslint-plugin-jest@^22.19.0: - version "22.19.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.19.0.tgz#0cf90946a8c927d40a2c64458c89bb635d0f2a0b" - integrity sha512-4zUc3rh36ds0SXdl2LywT4YWA3zRe8sfLhz8bPp8qQPIKvynTTkNGwmSCMpl5d9QiZE2JxSinGF+WD8yU+O0Lg== +eslint-plugin-jest@^23.3.0: + version "23.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.3.0.tgz#b1443d0c46d6a0de9ef3de78176dd6688c7d5326" + integrity sha512-GE6CR4ESJeu6Huw7vfZfaXHmX2R2kCFvf2X9OMcOxfP158yLKgLWz7PqLYTwRDACi84IhpmRxO8lK7GGwG05UQ== dependencies: - "@typescript-eslint/experimental-utils" "^1.13.0" + "@typescript-eslint/experimental-utils" "^2.5.0" eslint-plugin-jsx-a11y@^6.2.3: version "6.2.3" @@ -11558,10 +11541,10 @@ eslint-plugin-jsx-a11y@^6.2.3: has "^1.0.3" jsx-ast-utils "^2.2.1" -eslint-plugin-mocha@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-6.2.0.tgz#16ff9ce4d5a6a35af522d5db0ce3c8946566e4c1" - integrity sha512-vE/+tHJVom2BkMOiwkOKcAM5YqGPk3C6gMvQ32DHihKkaXF6vmxtj3UEOg64wP3m8/Zk5V/UmQbFE5nqu1EXSg== +eslint-plugin-mocha@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-6.2.2.tgz#6ef4b78bd12d744beb08a06e8209de330985100d" + integrity sha512-oNhPzfkT6Q6CJ0HMVJ2KLxEWG97VWGTmuHOoRcDLE0U88ugUyFNV9wrT2XIt5cGtqc5W9k38m4xTN34L09KhBA== dependencies: ramda "^0.26.1" @@ -11570,13 +11553,13 @@ eslint-plugin-no-unsanitized@^3.0.2: resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-3.0.2.tgz#83c6fcf8e34715112757e03dd4ee436dce29ed45" integrity sha512-JnwpoH8Sv4QOjrTDutENBHzSnyYtspdjtglYtqUtAHe6f6LLKqykJle+UwFPg23GGwt5hI3amS9CRDezW8GAww== -eslint-plugin-node@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" - integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== +eslint-plugin-node@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz#365944bb0804c5d1d501182a9bc41a0ffefed726" + integrity sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg== dependencies: - eslint-plugin-es "^2.0.0" - eslint-utils "^1.4.2" + eslint-plugin-es "^3.0.0" + eslint-utils "^2.0.0" ignore "^5.1.1" minimatch "^3.0.4" resolve "^1.10.1" @@ -11587,46 +11570,39 @@ eslint-plugin-prefer-object-spread@^1.2.1: resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-object-spread/-/eslint-plugin-prefer-object-spread-1.2.1.tgz#27fb91853690cceb3ae6101d9c8aecc6a67a402c" integrity sha1-J/uRhTaQzOs65hAdnIrsxqZ6QCw= -eslint-plugin-prettier@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz#507b8562410d02a03f0ddc949c616f877852f2ba" - integrity sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA== +eslint-plugin-prettier@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba" + integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA== dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.1.2.tgz#1358d2acb2c5e02b7e90c37e611ac258a488e3a7" - integrity sha512-ZR+AyesAUGxJAyTFlF3MbzeVHAcQTFQt1fFVe5o0dzY/HFoj1dgQDMoIkiM+ltN/HhlHBYX4JpJwYonjxsyQMA== +eslint-plugin-react-hooks@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a" + integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw== -eslint-plugin-react@^7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz#9928e4f3e2122ed3ba6a5b56d0303ba3e41d8c09" - integrity sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug== +eslint-plugin-react@^7.17.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz#a31b3e134b76046abe3cd278e7482bd35a1d12d7" + integrity sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A== dependencies: array-includes "^3.0.3" doctrine "^2.1.0" + eslint-plugin-eslint-plugin "^2.1.0" has "^1.0.3" - jsx-ast-utils "^2.2.1" + jsx-ast-utils "^2.2.3" object.entries "^1.1.0" - object.fromentries "^2.0.0" + object.fromentries "^2.0.1" object.values "^1.1.0" prop-types "^15.7.2" - resolve "^1.12.0" + resolve "^1.13.1" eslint-rule-composer@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== -eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" @@ -11643,13 +11619,20 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2, eslint-utils@^1.4.3: +eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" @@ -11699,10 +11682,10 @@ eslint@^2.7.0: text-table "~0.2.0" user-home "^2.0.0" -eslint@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6" - integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A== +eslint@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -11711,19 +11694,19 @@ eslint@^6.5.1: debug "^4.0.1" doctrine "^3.0.0" eslint-scope "^5.0.0" - eslint-utils "^1.4.2" + eslint-utils "^1.4.3" eslint-visitor-keys "^1.1.0" - espree "^6.1.1" + espree "^6.1.2" esquery "^1.0.1" esutils "^2.0.2" file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" - globals "^11.7.0" + globals "^12.1.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^6.4.1" + inquirer "^7.0.0" is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" @@ -11732,7 +11715,7 @@ eslint@^6.5.1: minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.2" + optionator "^0.8.3" progress "^2.0.0" regexpp "^2.0.1" semver "^6.1.2" @@ -11750,13 +11733,13 @@ espree@^3.1.6: acorn "^5.5.0" acorn-jsx "^3.0.0" -espree@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" - integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ== +espree@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" + integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== dependencies: - acorn "^7.0.0" - acorn-jsx "^5.0.2" + acorn "^7.1.0" + acorn-jsx "^5.1.0" eslint-visitor-keys "^1.1.0" esprima@2.7.x, esprima@^2.7.1: @@ -12380,11 +12363,16 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-memoize@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.1.tgz#c3519241e80552ce395e1a32dcdde8d1fd680f5d" + integrity sha512-xdmw296PCL01tMOXx9mdJSmWY29jQgxyuZdq0rEHMu+Tpe1eOEtCycoG6chzlcrWsNgpZP7oL8RiQr7+G6Bl6g== + fast-safe-stringify@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz#04b26106cc56681f51a044cfc0d76cf0008ac2c2" @@ -13832,7 +13820,7 @@ global@^4.4.0: min-document "^2.19.0" process "^0.11.10" -globals@^11.1.0, globals@^11.7.0: +globals@^11.1.0: version "11.7.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" integrity sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg== @@ -13842,6 +13830,13 @@ globals@^11.12.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" + integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== + dependencies: + type-fest "^0.8.1" + globals@^9.18.0, globals@^9.2.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -15746,25 +15741,6 @@ inquirer@^6.2.0: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^6.4.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - inquirer@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" @@ -17610,6 +17586,14 @@ jsx-ast-utils@^2.2.1: array-includes "^3.0.3" object.assign "^4.1.0" +jsx-ast-utils@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f" + integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA== + dependencies: + array-includes "^3.0.3" + object.assign "^4.1.0" + jsx-to-string@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jsx-to-string/-/jsx-to-string-1.4.0.tgz#66dc34d773dab9f40fe993cff9940e5da655b705" @@ -17629,6 +17613,11 @@ jszip@^3.1.5: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" +just-curry-it@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/just-curry-it/-/just-curry-it-3.1.0.tgz#ab59daed308a58b847ada166edd0a2d40766fbc5" + integrity sha512-mjzgSOFzlrurlURaHVjnQodyPNvrHrf1TbQP2XU9NSqBtHQPuHZ+Eb6TAJP7ASeJN9h9K0KXoRTs8u6ouHBKvg== + just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" @@ -18290,16 +18279,11 @@ load-source-map@^1.0.0: semver "^5.3.0" source-map "^0.5.6" -loader-runner@^2.3.0, loader-runner@^2.4.0: +loader-runner@^2.3.1, loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-runner@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.1.tgz#026f12fe7c3115992896ac02ba022ba92971b979" - integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw== - loader-utils@1.2.3, loader-utils@^1.0.4, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" @@ -18346,7 +18330,7 @@ locutus@^2.0.5: resolved "https://registry.yarnpkg.com/locutus/-/locutus-2.0.10.tgz#f903619466a98a4ab76e8b87a5854b55a743b917" integrity sha512-AZg2kCqrquMJ5FehDsBidV0qHl98NrsYtseUClzjAQ3HFnsDBJTCwGVplSQ82t9/QfgahqvTjaKcZqZkHmS0wQ== -lodash-es@^4.17.11, lodash-es@^4.17.4, lodash-es@^4.17.5, lodash-es@^4.2.1: +lodash-es@^4.17.11, lodash-es@^4.17.5, lodash-es@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== @@ -19221,7 +19205,7 @@ memory-fs@^0.2.0: resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" integrity sha1-8rslNovBIeORwlIN6Slpyu4KApA= -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -19325,7 +19309,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -20224,6 +20208,11 @@ node-forge@^0.7.6: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== +node-forge@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== + node-gyp@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" @@ -20297,7 +20286,7 @@ node-jose@1.1.0: util "^0.11.0" vm-browserify "0.0.4" -node-libs-browser@^2.0.0, node-libs-browser@^2.2.1: +node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== @@ -20806,7 +20795,7 @@ object.fromentries@^1.0.0: function-bind "^1.1.1" has "^1.0.1" -object.fromentries@^2.0.0, object.fromentries@^2.0.1: +object.fromentries@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== @@ -20992,7 +20981,7 @@ optional-js@^2.0.0: resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.1.1.tgz#c2dc519ad119648510b4d241dbb60b1167c36a46" integrity sha512-mUS4bDngcD5kKzzRUd1HVQkr9Lzzby3fSrrPR9wOHhQiyYo+hDS5NVli5YQzGjQRQ15k5Sno4xH9pfykJdeEUA== -optionator@^0.8.1, optionator@^0.8.2: +optionator@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= @@ -21004,6 +20993,18 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + ora@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" @@ -22955,6 +22956,13 @@ re-reselect@^3.4.0: resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd" integrity sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg== +re-resizable@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.1.1.tgz#7ff7cfe92c0b9d8b0bceaa578aadaeeff8931eaf" + integrity sha512-ngzX5xbXi9LlIghJUYZaBDkJUIMLYqO3tQ2cJZoNprCRGhfHnbyufKm51MZRIOBlLigLzPPFKBxQE8ZLezKGfA== + dependencies: + fast-memoize "^2.5.1" + react-ace@^5.5.0: version "5.10.0" resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.10.0.tgz#e328b37ac52759f700be5afdb86ada2f5ec84c5e" @@ -24085,25 +24093,21 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -reduce-reducers@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" - integrity sha1-+htHGLxSkqcd3R5dg5yb6pdw8Us= - reduce-reducers@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c" integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw== -redux-actions@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.2.1.tgz#d64186b25649a13c05478547d7cd7537b892410d" - integrity sha1-1kGGslZJoTwFR4VH1811N7iSQQ0= +redux-actions@2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.5.tgz#bdca548768ee99832a63910c276def85e821a27e" + integrity sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw== dependencies: - invariant "^2.2.1" - lodash "^4.13.1" - lodash-es "^4.17.4" - reduce-reducers "^0.1.0" + invariant "^2.2.4" + just-curry-it "^3.1.0" + loose-envify "^1.4.0" + reduce-reducers "^0.4.3" + to-camel-case "^1.0.0" redux-observable@^1.0.0: version "1.0.0" @@ -24838,6 +24842,13 @@ resolve@^1.12.0, resolve@^1.4.0: dependencies: path-parse "^1.0.6" +resolve@^1.13.1: + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== + dependencies: + path-parse "^1.0.6" + resolve@^1.5.0, resolve@^1.7.1: version "1.7.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" @@ -27345,7 +27356,7 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" -terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.4, terser-webpack-plugin@^1.4.1: +terser-webpack-plugin@^1.2.4, terser-webpack-plugin@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== @@ -27374,35 +27385,16 @@ terser-webpack-plugin@^2.1.2: terser "^4.3.4" webpack-sources "^1.4.3" -terser@^4.1.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.0.tgz#4b1b5f4424b426a7a47e80d6aae45e0d7979aef0" - integrity sha512-6lPt7lZdZ/13icQJp8XasFOwZjFJkxFFIb/N1fhYEQNoNI3Ilo3KABZ9OocZvZoB39r6SiIk/0+v/bt8nZoSeA== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.4.tgz#ad91bade95619e3434685d69efa621a5af5f877d" - integrity sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q== +terser@^4.1.2, terser@^4.3.4: + version "4.6.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.1.tgz#913e35e0d38a75285a7913ba01d753c4089ebdbd" + integrity sha512-w0f2OWFD7ka3zwetgVAhNMeyzEbj39ht2Tb0qKflw9PmW9Qbo5tjTh01QJLkhO9t9RDDQYvk+WXqpECI2C6i2A== dependencies: commander "^2.20.0" source-map "~0.6.1" source-map-support "~0.5.12" -test-exclude@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.1.0.tgz#6ba6b25179d2d38724824661323b73e03c0c1de1" - integrity sha512-gwf0S2fFsANC55fSeSqpb8BYk6w3FDvwZxfNjeF6FRgvFa43r+7wRiA/Q0IxoRU37wB/LE8IQ4221BsNucTaCA== - dependencies: - arrify "^1.0.1" - minimatch "^3.0.4" - read-pkg-up "^4.0.0" - require-main-filename "^1.0.1" - -test-exclude@^5.2.3: +test-exclude@^5.0.0, test-exclude@^5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== @@ -27681,6 +27673,13 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= +to-camel-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" + integrity sha1-GlYFSy+daWKYzmamCJcyK29CPkY= + dependencies: + to-space-case "^1.0.0" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -27691,6 +27690,11 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +to-no-case@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" + integrity sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo= + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -27739,6 +27743,13 @@ to-source-code@^1.0.0: dependencies: is-nil "^1.0.0" +to-space-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" + integrity sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc= + dependencies: + to-no-case "^1.0.0" + to-through@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" @@ -29902,7 +29913,7 @@ warning@^4.0.2: dependencies: loose-envify "^1.0.0" -watchpack@^1.5.0, watchpack@^1.6.0: +watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== @@ -30062,7 +30073,7 @@ webpack-sources@^1.1.0: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -30070,37 +30081,7 @@ webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack- source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.33.0: - version "4.33.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.33.0.tgz#c30fc4307db432e5c5e3333aaa7c16a15a3b277e" - integrity sha512-ggWMb0B2QUuYso6FPZKUohOgfm+Z0sVFs8WwWuSH1IAvkWs428VDNmOlAxvHGTB9Dm/qOB/qtE5cRx5y01clxw== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" - json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" - schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" - -webpack@4.41.0, webpack@^4.41.0: +webpack@4.41.0, webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.0: version "4.41.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b" integrity sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g== @@ -30129,35 +30110,6 @@ webpack@4.41.0, webpack@^4.41.0: watchpack "^1.6.0" webpack-sources "^1.4.1" -webpack@^4.33.0, webpack@^4.38.0: - version "4.39.3" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.39.3.tgz#a02179d1032156b713b6ec2da7e0df9d037def50" - integrity sha512-BXSI9M211JyCVc3JxHWDpze85CvjC842EvpRsVTc/d15YJGlox7GIDd38kJgWrb3ZluyvIjgenbLDMBQPDcxYQ== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.2.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.1" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.1" - watchpack "^1.6.0" - webpack-sources "^1.4.1" - websocket-driver@>=0.5.1: version "0.7.0" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" @@ -30371,6 +30323,11 @@ with@^5.0.0: acorn "^3.1.0" acorn-globals "^3.0.0" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"