diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index fa1e141be93eaa..6b8dc31bab34ed 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,99 +3,91 @@ 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 { +kibanaPipeline(timeoutMinutes: 180) { + catchErrors { + withEnv([ + 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + ]) { + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': { withEnv([ - 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + 'NODE_ENV=test' // Needed for jest tests only ]) { - parallel([ - 'kibana-intake-agent': { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - }, - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, - '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), - ]), - ]) - 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 - tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage - # 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') - } + workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() } - } - kibanaPipeline.sendMail() + }, + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + ]), + ]) + workers.base(name: 'coverage-worker', label: 'tests-l', ramDisk: false, bootstrapped: 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 + tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage + # 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 f702405aad69e3..befb8d259b5b63 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -21,53 +21,47 @@ def workerFailures = [] currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) currentBuild.description = "${params.CI_GROUP}
Agents: ${AGENT_COUNT}
Executions: ${params.NUMBER_EXECUTIONS}" -stage("Kibana Pipeline") { - timeout(time: 180, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - def agents = [:] - for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { - def agentNumberInside = agentNumber - def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) - agents["agent-${agentNumber}"] = { - catchError { - print "Agent ${agentNumberInside} - ${agentExecutions} executions" - - kibanaPipeline.withWorkers('flaky-test-runner', { - if (NEED_BUILD) { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") - } - } else { - kibanaPipeline.buildXpack() - } - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() +kibanaPipeline(timeoutMinutes: 180) { + def agents = [:] + for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { + def agentNumberInside = agentNumber + def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) + agents["agent-${agentNumber}"] = { + catchErrors { + print "Agent ${agentNumberInside} - ${agentExecutions} executions" + + workers.functional('flaky-test-runner', { + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } } - } + }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + } + } + } - parallel(agents) + parallel(agents) - currentBuild.description += ", Failures: ${workerFailures.size()}" + currentBuild.description += ", Failures: ${workerFailures.size()}" - if (workerFailures.size() > 0) { - print "There were ${workerFailures.size()} test suite failures." - print "The executions that failed were:" - print workerFailures.join("\n") - print "Please check 'Test Result' and 'Pipeline Steps' pages for more info" - } - } - } + if (workerFailures.size() > 0) { + print "There were ${workerFailures.size()} test suite failures." + print "The executions that failed were:" + print workerFailures.join("\n") + print "Please check 'Test Result' and 'Pipeline Steps' pages for more info" } } def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'serverMocha') { - return kibanaPipeline.getPostBuildWorker('serverMocha', { + return kibanaPipeline.functionalTestProcess('serverMocha', { kibanaPipeline.bash( """ source src/dev/ci_setup/setup_env.sh @@ -77,20 +71,20 @@ def getWorkerFromParams(isXpack, job, ciGroup) { ) }) } else if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) + return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) + return kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh') } else { - return kibanaPipeline.getOssCiGroupWorker(ciGroup) + return kibanaPipeline.ossCiGroupProcess(ciGroup) } } if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }) + return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }) + return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') } else { - return kibanaPipeline.getXpackCiGroupWorker(ciGroup) + return kibanaPipeline.xpackCiGroupProcess(ciGroup) } } @@ -105,10 +99,9 @@ def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWor for(def j = 0; j < workerExecutions; j++) { print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" withEnv([ - "JOB=agent-${agentNumber}-worker-${workerNumber}-${j}", "REMOVE_KIBANA_INSTALL_DIR=1", ]) { - catchError { + catchErrors { try { worker(workerNumber) } catch (ex) { diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_visual_baseline new file mode 100644 index 00000000000000..4a1e0f7d74e078 --- /dev/null +++ b/.ci/Jenkinsfile_visual_baseline @@ -0,0 +1,21 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +kibanaPipeline(timeoutMinutes: 120) { + catchError { + parallel([ + workers.base(name: 'oss-visualRegression', label: 'linux && immutable') { + kibanaPipeline.buildOss() + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh') + }, + workers.base(name: 'xpack-visualRegression', label: 'linux && immutable') { + kibanaPipeline.buildXpack() + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') + }, + ]) + } + + kibanaPipeline.sendMail() +} diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index ad0ad54275e12d..a00bcb3bbc9465 100644 --- a/.ci/es-snapshots/Jenkinsfile_build_es +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -26,7 +26,7 @@ timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { node('linux && immutable') { - catchError { + catchErrors { def VERSION def SNAPSHOT_ID def DESTINATION diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 30d52a56547bd2..ce472a404c0538 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -19,50 +19,45 @@ currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" -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.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - '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() +kibanaPipeline(timeoutMinutes: 120) { + catchErrors { + withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + ]), + ]) } + + promoteSnapshot(SNAPSHOT_VERSION, SNAPSHOT_ID) } + + kibanaPipeline.sendMail() } def promoteSnapshot(snapshotVersion, snapshotId) { diff --git a/.eslintrc.js b/.eslintrc.js index c41c36f0720cba..2609b538cc2d8f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,12 +76,6 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['src/legacy/core_plugins/vis_type_vislib/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: [ 'src/legacy/core_plugins/vis_default_editor/public/components/controls/**/*.{ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de46bcfa698308..5948b9672e6d4b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,7 +5,6 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app -/src/plugins/share/ @elastic/kibana-app /src/legacy/server/url_shortening/ @elastic/kibana-app /src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app @@ -27,6 +26,7 @@ /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app /src/plugins/dev_tools/ @elastic/kibana-app +/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app # App Architecture /packages/kbn-interpreter/ @elastic/kibana-app-arch @@ -42,7 +42,6 @@ /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch -/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch /src/plugins/expressions/ @elastic/kibana-app-arch @@ -53,6 +52,9 @@ /src/plugins/navigation/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch +/src/plugins/share/ @elastic/kibana-app-arch +/examples/url_generators_examples/ @elastic/kibana-app-arch +/examples/url_generators_explorer/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch @@ -80,12 +82,14 @@ # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui +/x-pack/plugins/ml/ @elastic/ml-ui /x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/ml.ts @elastic/ml-ui # ML team owns the transform plugin, ES team added here for visibility # because the plugin lives in Kibana's Elasticsearch management section. /x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui +/x-pack/plugins/transform/ @elastic/ml-ui @elastic/es-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform_ui/ @elastic/ml-ui /x-pack/test/functional/services/transform.ts @elastic/ml-ui diff --git a/.gitignore b/.gitignore index 02b20da297fc67..efb5c577746334 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,3 @@ package-lock.json *.sublime-* npm-debug.log* .tern-project -x-pack/legacy/plugins/apm/tsconfig.json -apm.tsconfig.json diff --git a/Jenkinsfile b/Jenkinsfile index 1b4350d5b91e90..742aec1d4e7ab4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,71 +3,50 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit - timeout(time: 135, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - githubPr.withDefaultPrComments { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - // 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { - // retryable('kibana-firefoxSmoke') { - // runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') - // } - // }), - '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), - 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { - retryable('kibana-accessibility') { - runbld('./test/scripts/jenkins_accessibility.sh', 'Execute kibana-accessibility') - } - }), - // 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - // 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { - // retryable('xpack-firefoxSmoke') { - // runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') - // } - // }), - '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), - 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { - retryable('xpack-accessibility') { - runbld('./test/scripts/jenkins_xpack_accessibility.sh', 'Execute xpack-accessibility') - } - }), - // 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }), - ]), - ]) - } - } - - retryable.printFlakyFailures() - kibanaPipeline.sendMail() - } +kibanaPipeline(timeoutMinutes: 135) { + githubPr.withDefaultPrComments { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + // 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + // 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-siemCypress': kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh'), + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) } } + + retryable.printFlakyFailures() + kibanaPipeline.sendMail() } diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md index 9a3fa1a1bb48a6..f22e70b0e7bee2 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md @@ -44,7 +44,6 @@ import { MyPluginDepsStart } from './plugin'; export renderApp = ({ element, history }: AppMountParameters) => { ReactDOM.render( - // pass `appBasePath` to `basename` , diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md index 283ae34f14c545..6c5b89ffda05be 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md @@ -26,7 +26,7 @@ import { BrowserRouter, Route } from 'react-router-dom'; import { CoreStart, AppMountParams } from 'src/core/public'; import { MyPluginDepsStart } from './plugin'; -export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { +export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { const { renderApp, hasUnsavedChanges } = await import('./application'); onAppLeave(actions => { if(hasUnsavedChanges()) { @@ -34,7 +34,7 @@ export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { } return actions.default(); }); - return renderApp(params); + return renderApp({ element, history }); } ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index c36d649837e8a8..fa052c1179a301 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -20,6 +20,7 @@ export interface CoreSetup | [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | +| [metrics](./kibana-plugin-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | | [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | | [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-server.coresetup.metrics.md new file mode 100644 index 00000000000000..5db723751be858 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.metrics.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [metrics](./kibana-plugin-server.coresetup.metrics.md) + +## CoreSetup.metrics property + +[MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) + +Signature: + +```typescript +metrics: MetricsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-server.destructiveroutemethod.md b/docs/development/core/server/kibana-plugin-server.destructiveroutemethod.md new file mode 100644 index 00000000000000..48b1e837f6db95 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.destructiveroutemethod.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DestructiveRouteMethod](./kibana-plugin-server.destructiveroutemethod.md) + +## DestructiveRouteMethod type + +Set of HTTP methods changing the state of the server. + +Signature: + +```typescript +export declare type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.exportsavedobjectstostream.md b/docs/development/core/server/kibana-plugin-server.exportsavedobjectstostream.md new file mode 100644 index 00000000000000..76f0cea20d637e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.exportsavedobjectstostream.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [exportSavedObjectsToStream](./kibana-plugin-server.exportsavedobjectstostream.md) + +## exportSavedObjectsToStream() function + +Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-server.savedobjectsexportoptions.md) for more detailed information. + +Signature: + +```typescript +export declare function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, } | SavedObjectsExportOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-server.importsavedobjectsfromstream.md new file mode 100644 index 00000000000000..2293e196ae61e5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.importsavedobjectsfromstream.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [importSavedObjectsFromStream](./kibana-plugin-server.importsavedobjectsfromstream.md) + +## importSavedObjectsFromStream() function + +Import saved objects from given stream. See the [options](./kibana-plugin-server.savedobjectsimportoptions.md) for more detailed information. + +Signature: + +```typescript +export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, } | SavedObjectsImportOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 15a1fd05062568..e843ffb265b826 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -37,6 +37,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResultType](./kibana-plugin-server.authresulttype.md) | | | [AuthStatus](./kibana-plugin-server.authstatus.md) | Status indicating an outcome of the authentication. | +## Functions + +| Function | Description | +| --- | --- | +| [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-server.savedobjectsexportoptions.md) for more detailed information. | +| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-server.savedobjectsimportoptions.md) for more detailed information. | +| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | + ## Interfaces | Interface | Description | @@ -88,11 +96,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | +| [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | | [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) | Response status code. | | [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [OpsMetrics](./kibana-plugin-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | +| [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) | OS related metrics | +| [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) | Process related metrics | +| [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) | server related metrics | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | @@ -183,6 +196,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) | Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | | [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) | A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md).See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. | | [ConfigPath](./kibana-plugin-server.configpath.md) | | +| [DestructiveRouteMethod](./kibana-plugin-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | @@ -227,6 +241,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) | The custom validation function if @kbn/config-schema is not a valid solution for your specific plugin requirements. | | [RouteValidationSpec](./kibana-plugin-server.routevalidationspec.md) | Allowed property validation options: either @kbn/config-schema validations or custom validation functionsSee [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) for custom validation. | | [RouteValidatorFullConfig](./kibana-plugin-server.routevalidatorfullconfig.md) | Route validations config and options merged into one object | +| [SafeRouteMethod](./kibana-plugin-server.saferoutemethod.md) | Set of HTTP methods not changing the state of the server. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-server.savedobjectstype.md) used to migrate it to a given version | diff --git a/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md new file mode 100644 index 00000000000000..454b8c905451ee --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-server.metricsservicesetup.getopsmetrics_.md) + +## MetricsServiceSetup.getOpsMetrics$ property + +Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. + +Signature: + +```typescript +getOpsMetrics$: () => Observable; +``` + +## Example + + +```ts +core.metrics.getOpsMetrics$().subscribe(metrics => { + // do something with the metrics +}) + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md new file mode 100644 index 00000000000000..270c56402a390d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) + +## MetricsServiceSetup interface + +APIs to retrieves metrics gathered and exposed by the core platform. + +Signature: + +```typescript +export interface MetricsServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getOpsMetrics$](./kibana-plugin-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | + diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md new file mode 100644 index 00000000000000..cfd39a551ad349 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [concurrent\_connections](./kibana-plugin-server.opsmetrics.concurrent_connections.md) + +## OpsMetrics.concurrent\_connections property + +number of current concurrent connections to the server + +Signature: + +```typescript +concurrent_connections: OpsServerMetrics['concurrent_connections']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.md new file mode 100644 index 00000000000000..e23bd8d431d3fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) + +## OpsMetrics interface + +Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. + +Signature: + +```typescript +export interface OpsMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [concurrent\_connections](./kibana-plugin-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics['concurrent_connections'] | number of current concurrent connections to the server | +| [os](./kibana-plugin-server.opsmetrics.os.md) | OpsOsMetrics | OS related metrics | +| [process](./kibana-plugin-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics | +| [requests](./kibana-plugin-server.opsmetrics.requests.md) | OpsServerMetrics['requests'] | server requests stats | +| [response\_times](./kibana-plugin-server.opsmetrics.response_times.md) | OpsServerMetrics['response_times'] | server response time stats | + diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md new file mode 100644 index 00000000000000..993a1d7a2d7b79 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [os](./kibana-plugin-server.opsmetrics.os.md) + +## OpsMetrics.os property + +OS related metrics + +Signature: + +```typescript +os: OpsOsMetrics; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md new file mode 100644 index 00000000000000..53d3a33d66e06a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [process](./kibana-plugin-server.opsmetrics.process.md) + +## OpsMetrics.process property + +Process related metrics + +Signature: + +```typescript +process: OpsProcessMetrics; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md new file mode 100644 index 00000000000000..9cd6b85e507f0c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [requests](./kibana-plugin-server.opsmetrics.requests.md) + +## OpsMetrics.requests property + +server requests stats + +Signature: + +```typescript +requests: OpsServerMetrics['requests']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md new file mode 100644 index 00000000000000..358699071b1c3b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [response\_times](./kibana-plugin-server.opsmetrics.response_times.md) + +## OpsMetrics.response\_times property + +server response time stats + +Signature: + +```typescript +response_times: OpsServerMetrics['response_times']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md new file mode 100644 index 00000000000000..338164f173d025 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [distro](./kibana-plugin-server.opsosmetrics.distro.md) + +## OpsOsMetrics.distro property + +The os distrib. Only present for linux platforms + +Signature: + +```typescript +distro?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md new file mode 100644 index 00000000000000..24c5a1f00b64c9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [distroRelease](./kibana-plugin-server.opsosmetrics.distrorelease.md) + +## OpsOsMetrics.distroRelease property + +The os distrib release, prefixed by the os distrib. Only present for linux platforms + +Signature: + +```typescript +distroRelease?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md new file mode 100644 index 00000000000000..0bf17502ce34e4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [load](./kibana-plugin-server.opsosmetrics.load.md) + +## OpsOsMetrics.load property + +cpu load metrics + +Signature: + +```typescript +load: { + '1m': number; + '5m': number; + '15m': number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.md new file mode 100644 index 00000000000000..0fb4e59fdf539a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) + +## OpsOsMetrics interface + +OS related metrics + +Signature: + +```typescript +export interface OpsOsMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [distro](./kibana-plugin-server.opsosmetrics.distro.md) | string | The os distrib. Only present for linux platforms | +| [distroRelease](./kibana-plugin-server.opsosmetrics.distrorelease.md) | string | The os distrib release, prefixed by the os distrib. Only present for linux platforms | +| [load](./kibana-plugin-server.opsosmetrics.load.md) | {
'1m': number;
'5m': number;
'15m': number;
} | cpu load metrics | +| [memory](./kibana-plugin-server.opsosmetrics.memory.md) | {
total_in_bytes: number;
free_in_bytes: number;
used_in_bytes: number;
} | system memory usage metrics | +| [platform](./kibana-plugin-server.opsosmetrics.platform.md) | NodeJS.Platform | The os platform | +| [platformRelease](./kibana-plugin-server.opsosmetrics.platformrelease.md) | string | The os platform release, prefixed by the platform name | +| [uptime\_in\_millis](./kibana-plugin-server.opsosmetrics.uptime_in_millis.md) | number | the OS uptime | + diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md new file mode 100644 index 00000000000000..4a1becaeeaec71 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [memory](./kibana-plugin-server.opsosmetrics.memory.md) + +## OpsOsMetrics.memory property + +system memory usage metrics + +Signature: + +```typescript +memory: { + total_in_bytes: number; + free_in_bytes: number; + used_in_bytes: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md new file mode 100644 index 00000000000000..411d0fc546dc0b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [platform](./kibana-plugin-server.opsosmetrics.platform.md) + +## OpsOsMetrics.platform property + +The os platform + +Signature: + +```typescript +platform: NodeJS.Platform; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md new file mode 100644 index 00000000000000..1071b4a38f5880 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [platformRelease](./kibana-plugin-server.opsosmetrics.platformrelease.md) + +## OpsOsMetrics.platformRelease property + +The os platform release, prefixed by the platform name + +Signature: + +```typescript +platformRelease: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md new file mode 100644 index 00000000000000..dfff1a1f1da0ba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [uptime\_in\_millis](./kibana-plugin-server.opsosmetrics.uptime_in_millis.md) + +## OpsOsMetrics.uptime\_in\_millis property + +the OS uptime + +Signature: + +```typescript +uptime_in_millis: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md new file mode 100644 index 00000000000000..f61c8b0995324d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [event\_loop\_delay](./kibana-plugin-server.opsprocessmetrics.event_loop_delay.md) + +## OpsProcessMetrics.event\_loop\_delay property + +node event loop delay + +Signature: + +```typescript +event_loop_delay: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md new file mode 100644 index 00000000000000..92fd8471cce7da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) + +## OpsProcessMetrics interface + +Process related metrics + +Signature: + +```typescript +export interface OpsProcessMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [event\_loop\_delay](./kibana-plugin-server.opsprocessmetrics.event_loop_delay.md) | number | node event loop delay | +| [memory](./kibana-plugin-server.opsprocessmetrics.memory.md) | {
heap: {
total_in_bytes: number;
used_in_bytes: number;
size_limit: number;
};
resident_set_size_in_bytes: number;
} | process memory usage | +| [pid](./kibana-plugin-server.opsprocessmetrics.pid.md) | number | pid of the kibana process | +| [uptime\_in\_millis](./kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md) | number | uptime of the kibana process | + diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md new file mode 100644 index 00000000000000..5c1a8de70dc019 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [memory](./kibana-plugin-server.opsprocessmetrics.memory.md) + +## OpsProcessMetrics.memory property + +process memory usage + +Signature: + +```typescript +memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md new file mode 100644 index 00000000000000..a34187f3720188 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [pid](./kibana-plugin-server.opsprocessmetrics.pid.md) + +## OpsProcessMetrics.pid property + +pid of the kibana process + +Signature: + +```typescript +pid: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md new file mode 100644 index 00000000000000..24db2f017a663e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [uptime\_in\_millis](./kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md) + +## OpsProcessMetrics.uptime\_in\_millis property + +uptime of the kibana process + +Signature: + +```typescript +uptime_in_millis: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md new file mode 100644 index 00000000000000..ade79fedfa1b5c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [concurrent\_connections](./kibana-plugin-server.opsservermetrics.concurrent_connections.md) + +## OpsServerMetrics.concurrent\_connections property + +number of current concurrent connections to the server + +Signature: + +```typescript +concurrent_connections: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.md new file mode 100644 index 00000000000000..4e35c02bd9f28a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) + +## OpsServerMetrics interface + +server related metrics + +Signature: + +```typescript +export interface OpsServerMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [concurrent\_connections](./kibana-plugin-server.opsservermetrics.concurrent_connections.md) | number | number of current concurrent connections to the server | +| [requests](./kibana-plugin-server.opsservermetrics.requests.md) | {
disconnects: number;
total: number;
statusCodes: Record<number, number>;
} | server requests stats | +| [response\_times](./kibana-plugin-server.opsservermetrics.response_times.md) | {
avg_in_millis: number;
max_in_millis: number;
} | server response time stats | + diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md new file mode 100644 index 00000000000000..5ad2abc8695577 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [requests](./kibana-plugin-server.opsservermetrics.requests.md) + +## OpsServerMetrics.requests property + +server requests stats + +Signature: + +```typescript +requests: { + disconnects: number; + total: number; + statusCodes: Record; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md new file mode 100644 index 00000000000000..5008efc6ad4da2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [response\_times](./kibana-plugin-server.opsservermetrics.response_times.md) + +## OpsServerMetrics.response\_times property + +server response time stats + +Signature: + +```typescript +response_times: { + avg_in_millis: number; + max_in_millis: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-server.resolvesavedobjectsimporterrors.md new file mode 100644 index 00000000000000..9fcb335aad5568 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.resolvesavedobjectsimporterrors.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [resolveSavedObjectsImportErrors](./kibana-plugin-server.resolvesavedobjectsimporterrors.md) + +## resolveSavedObjectsImportErrors() function + +Resolve and return saved object import errors. See the [options](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. + +Signature: + +```typescript +export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, } | SavedObjectsResolveImportErrorsOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index 0929e15b6228b7..7fbab90cc2c8a6 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -19,4 +19,5 @@ export interface RouteConfigOptions | [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | | [body](./kibana-plugin-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | +| [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) | Method extends 'get' ? never : boolean | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain kbn-xsrf header. - false. Disables xsrf protection.Set to true by default | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.xsrfrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.xsrfrequired.md new file mode 100644 index 00000000000000..801a0c3dc299b9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.xsrfrequired.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) + +## RouteConfigOptions.xsrfRequired property + +Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain `kbn-xsrf` header. - false. Disables xsrf protection. + +Set to true by default + +Signature: + +```typescript +xsrfRequired?: Method extends 'get' ? never : boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routemethod.md b/docs/development/core/server/kibana-plugin-server.routemethod.md index 939ae94b85691b..ed0d8e9af4b194 100644 --- a/docs/development/core/server/kibana-plugin-server.routemethod.md +++ b/docs/development/core/server/kibana-plugin-server.routemethod.md @@ -9,5 +9,5 @@ The set of common HTTP methods supported by Kibana routing. Signature: ```typescript -export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; +export declare type RouteMethod = SafeRouteMethod | DestructiveRouteMethod; ``` diff --git a/docs/development/core/server/kibana-plugin-server.saferoutemethod.md b/docs/development/core/server/kibana-plugin-server.saferoutemethod.md new file mode 100644 index 00000000000000..432aa4c6e70147 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.saferoutemethod.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SafeRouteMethod](./kibana-plugin-server.saferoutemethod.md) + +## SafeRouteMethod type + +Set of HTTP methods not changing the state of the server. + +Signature: + +```typescript +export declare type SafeRouteMethod = 'get' | 'options'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md index 9653fa802a3e85..013773e0789a1e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-server.savedobjectsimportoptions.namespace.md) | string | | -| [objectLimit](./kibana-plugin-server.savedobjectsimportoptions.objectlimit.md) | number | | -| [overwrite](./kibana-plugin-server.savedobjectsimportoptions.overwrite.md) | boolean | | -| [readStream](./kibana-plugin-server.savedobjectsimportoptions.readstream.md) | Readable | | -| [savedObjectsClient](./kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | | -| [supportedTypes](./kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md) | string[] | | +| [namespace](./kibana-plugin-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | +| [objectLimit](./kibana-plugin-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | +| [overwrite](./kibana-plugin-server.savedobjectsimportoptions.overwrite.md) | boolean | if true, will override existing object if present | +| [readStream](./kibana-plugin-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-server.savedobject.md) to import | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-server.savedobjectsclientcontract.md) to use to perform the import operation | +| [supportedTypes](./kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md) | string[] | the list of allowed types to import | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md index 2b15ba2a1b7ec3..bf8e56f65607c4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.namespace property +if specified, will import in given namespace, else will import as global object + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md index 89c01a13644b86..526aef96eb8c0e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.objectLimit property +The maximum number of object to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md index 54728aaa80fed5..3a84001bbbad49 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.overwrite property +if true, will override existing object if present + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md index 7739fdfbc8460b..64875d42515aaa 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.readStream property +The stream of [saved objects](./kibana-plugin-server.savedobject.md) to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md index 23d5aba5fe1144..864fe64f53a4e2 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.savedObjectsClient property +[client](./kibana-plugin-server.savedobjectsclientcontract.md) to use to perform the import operation + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md index 03ee12ab2a0f7d..a897551bfcb12e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.supportedTypes property +the list of allowed types to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md index 8ed978d4a2639a..75c9d77c5bf676 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md @@ -16,10 +16,10 @@ export interface SavedObjectsResolveImportErrorsOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | | -| [objectLimit](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | | -| [readStream](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | | -| [retries](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | | -| [savedObjectsClient](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | | -| [supportedTypes](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) | string[] | | +| [namespace](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | +| [objectLimit](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | The maximum number of object to import | +| [readStream](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-server.savedobject.md) to resolve errors from | +| [retries](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | +| [supportedTypes](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) | string[] | the list of allowed types to import | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md index b88f124545bd58..87b69c78b33ee9 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.namespace property +if specified, will import in given namespace + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md index a2753ceccc36fe..57a3c358406d86 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.objectLimit property +The maximum number of object to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md index e7a31deed6faa3..f109816c0de54b 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.readStream property +The stream of [saved objects](./kibana-plugin-server.savedobject.md) to resolve errors from + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md index 658aa64cfc33f9..265dd21b3728a1 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.retries property +saved object import references to retry + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md index 8a8c620b2cf217..9a1864bfbbcd6c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.savedObjectsClient property +client to use to perform the import operation + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md index 9cc97c34669b7e..e5db98aec23d9c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.supportedTypes property +the list of allowed types to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md new file mode 100644 index 00000000000000..d8ec90d1718dc5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) > [getImportExportObjectLimit](./kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md) + +## SavedObjectsServiceSetup.getImportExportObjectLimit property + +Returns the maximum number of objects allowed for import or export operations. + +Signature: + +```typescript +getImportExportObjectLimit: () => number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md index 963c4bbeb55153..2cc070d105d9f7 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md @@ -54,6 +54,7 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) with the given priority. | +| [getImportExportObjectLimit](./kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md) | () => number | Returns the maximum number of objects allowed for import or export operations. | | [registerType](./kibana-plugin-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index c698e2db86ddb1..f62a4d28dfc0df 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -116,7 +116,8 @@ cluster alert notifications from Monitoring. ==== Dashboard [horizontal] -`xpackDashboardMode:roles`:: The roles that belong to <>. +`xpackDashboardMode:roles`:: **Deprecated. Use <> instead.** +The roles that belong to <>. [float] [[kibana-discover-settings]] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 3843cc27defd5d..8ad5330f3fda59 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -56,6 +56,11 @@ This page has moved. Please see <>. This page has moved. Please see <>. +[role="exclude",id="add-sample-data"] +== Add sample data + +This page has moved. Please see <>. + [role="exclude",id="tilemap"] == Coordinate map diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc index c6fe5b5b92d696..d426ec111351cc 100644 --- a/docs/user/getting-started.asciidoc +++ b/docs/user/getting-started.asciidoc @@ -1,54 +1,65 @@ [[getting-started]] -= Getting Started += Get started [partintro] -- -You’re new to Kibana and want to give it a try. {kib} has sample data sets and -tutorials to help you get started. +Ready to try out {kib} and see what it can do? To quickest way to get started with {kib} is to set up on Cloud, then add a sample data set that helps you get a handle on the full range of {kib} features. [float] -=== Sample data +[[cloud-set-up]] +== Set up on Cloud -You can use the <> to take {kib} for a test ride without having -to go through the process of loading data yourself. With one click, -you can install a sample data set and start interacting with -{kib} visualizations in seconds. You can access the sample data -from the {kib} home page. +To access {kib} in a single click, run our hosted Elasticsearch Service on Elastic Cloud. -[float] +. Log into the link:https://cloud.elastic.co/[Elasticsearch Service Console]. +If you need an account, register for a link:https://www.elastic.co/cloud/elasticsearch-service/signup[free 14-day trial]. + +. Click *Create deployment*, then give your deployment a name. -=== Add data tutorials -{kib} has built-in *Add Data* tutorials to help you set up -data flows in the Elastic Stack. These tutorials are available -from the Kibana home page. In *Add Data to Kibana*, find the data type -you’re interested in, and click its button to view a list of available tutorials. +. To use the default options, click *Create deployment*. You can modify the other deployment options, but the default options are great to get started. + +Be sure to copy down the password for the `elastic` user and Cloud ID information. You'll need that later. [float] -=== Hands-on experience +[[get-data-in]] +== Get data into {kib} + +The easiest way to get data into {kib} is to add a sample data set. + +{kib} has several sample data sets that you can use before loading your own data: + +* *Sample eCommerce orders* includes visualizations for tracking product-related information, +such as cost, revenue, and price. + +* *Sample flight data* includes visualizations for monitoring flight routes. -The following tutorials walk you through searching, analyzing, -and visualizing data. +* *Sample web logs* includes visualizations for monitoring website traffic. -* <>. You'll -learn to filter and query data, edit visualizations, and interact with dashboards. +To use the sample data sets: -* <>. You'll manually load a data set and build -your own visualizations and dashboard. +. Go to the {kib} home page. + +. Click *Load a data set and a {kib} dashboard*. + +. Click *View data* and view the prepackaged dashboards, maps, and more. + +[role="screenshot"] +image::images/add-sample-data.png[] + +NOTE: The timestamps in the sample data sets are relative to when they are installed. +If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. [float] -=== Before you begin +[[getting-started-next-steps]] +== Next steps -Make sure you've <> and established -a <>. +* To get a hands-on experience creating visualizations, follow the <> tutorial. -If you are running our hosted Elasticsearch Service on Elastic Cloud, you access Kibana with a single click. (You can {ess-trial}[sign up for a free trial] and start exploring data in minutes.) +* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. -- -include::{kib-repo-dir}/getting-started/add-sample-data.asciidoc[] - include::{kib-repo-dir}/getting-started/tutorial-sample-data.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-full-experience.asciidoc[] @@ -60,4 +71,3 @@ include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-dashboard.asciidoc[] - diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 3911d57e05c9a4..ff100d07633686 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -1,13 +1,13 @@ include::introduction.asciidoc[] +include::getting-started.asciidoc[] + include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[] include::security/securing-kibana.asciidoc[] -include::getting-started.asciidoc[] - include::discover.asciidoc[] include::visualize.asciidoc[] diff --git a/examples/ui_action_examples/public/hello_world_action.tsx b/examples/ui_action_examples/public/hello_world_action.tsx index f4c3bfeee6a6dd..da20f404645167 100644 --- a/examples/ui_action_examples/public/hello_world_action.tsx +++ b/examples/ui_action_examples/public/hello_world_action.tsx @@ -22,7 +22,7 @@ import { OverlayStart } from '../../../src/core/public'; import { createAction } from '../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../src/plugins/kibana_react/public'; -export const HELLO_WORLD_ACTION_TYPE = 'HELLO_WORLD_ACTION_TYPE'; +export const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD'; interface StartServices { openModal: OverlayStart['openModal']; @@ -30,7 +30,7 @@ interface StartServices { export const createHelloWorldAction = (getStartServices: () => Promise) => createAction({ - type: HELLO_WORLD_ACTION_TYPE, + type: ACTION_HELLO_WORLD, getDisplayName: () => 'Hello World!', execute: async () => { const { openModal } = await getStartServices(); diff --git a/examples/ui_action_examples/public/index.ts b/examples/ui_action_examples/public/index.ts index 9dce2191d2670d..88a36d278e256e 100644 --- a/examples/ui_action_examples/public/index.ts +++ b/examples/ui_action_examples/public/index.ts @@ -23,4 +23,4 @@ import { PluginInitializer } from '../../../src/core/public'; export const plugin: PluginInitializer = () => new UiActionExamplesPlugin(); export { HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; -export { HELLO_WORLD_ACTION_TYPE } from './hello_world_action'; +export { ACTION_HELLO_WORLD } from './hello_world_action'; diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts index 08b65714dbf665..c47746d4b3fd6e 100644 --- a/examples/ui_action_examples/public/plugin.ts +++ b/examples/ui_action_examples/public/plugin.ts @@ -19,7 +19,7 @@ import { Plugin, CoreSetup } from '../../../src/core/public'; import { UiActionsSetup } from '../../../src/plugins/ui_actions/public'; -import { createHelloWorldAction } from './hello_world_action'; +import { createHelloWorldAction, ACTION_HELLO_WORLD } from './hello_world_action'; import { helloWorldTrigger, HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; interface UiActionExamplesSetupDependencies { @@ -28,7 +28,11 @@ interface UiActionExamplesSetupDependencies { declare module '../../../src/plugins/ui_actions/public' { export interface TriggerContextMapping { - [HELLO_WORLD_TRIGGER_ID]: undefined; + [HELLO_WORLD_TRIGGER_ID]: {}; + } + + export interface ActionContextMapping { + [ACTION_HELLO_WORLD]: {}; } } @@ -42,7 +46,7 @@ export class UiActionExamplesPlugin })); uiActions.registerAction(helloWorldAction); - uiActions.attachAction(helloWorldTrigger.id, helloWorldAction.id); + uiActions.attachAction(helloWorldTrigger.id, helloWorldAction); } public start() {} diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index 2770b0e3bd5ff2..64a820ab6d1947 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -27,44 +27,48 @@ export const USER_TRIGGER = 'USER_TRIGGER'; export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER'; export const PHONE_TRIGGER = 'PHONE_TRIGGER'; -export const VIEW_IN_MAPS_ACTION = 'VIEW_IN_MAPS_ACTION'; -export const TRAVEL_GUIDE_ACTION = 'TRAVEL_GUIDE_ACTION'; -export const CALL_PHONE_NUMBER_ACTION = 'CALL_PHONE_NUMBER_ACTION'; -export const EDIT_USER_ACTION = 'EDIT_USER_ACTION'; -export const PHONE_USER_ACTION = 'PHONE_USER_ACTION'; -export const SHOWCASE_PLUGGABILITY_ACTION = 'SHOWCASE_PLUGGABILITY_ACTION'; +export const ACTION_VIEW_IN_MAPS = 'ACTION_VIEW_IN_MAPS'; +export const ACTION_TRAVEL_GUIDE = 'ACTION_TRAVEL_GUIDE'; +export const ACTION_CALL_PHONE_NUMBER = 'ACTION_CALL_PHONE_NUMBER'; +export const ACTION_EDIT_USER = 'ACTION_EDIT_USER'; +export const ACTION_PHONE_USER = 'ACTION_PHONE_USER'; +export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; -export const showcasePluggability = createAction({ - type: SHOWCASE_PLUGGABILITY_ACTION, +export const showcasePluggability = createAction({ + type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', execute: async () => alert("Isn't that cool?!"), }); -export type PhoneContext = string; +export interface PhoneContext { + phone: string; +} -export const makePhoneCallAction = createAction({ - type: CALL_PHONE_NUMBER_ACTION, +export const makePhoneCallAction = createAction({ + type: ACTION_CALL_PHONE_NUMBER, getDisplayName: () => 'Call phone number', - execute: async phone => alert(`Pretend calling ${phone}...`), + execute: async context => alert(`Pretend calling ${context.phone}...`), }); -export const lookUpWeatherAction = createAction<{ country: string }>({ - type: TRAVEL_GUIDE_ACTION, +export const lookUpWeatherAction = createAction({ + type: ACTION_TRAVEL_GUIDE, getIconType: () => 'popout', getDisplayName: () => 'View travel guide', - execute: async ({ country }) => { - window.open(`https://www.worldtravelguide.net/?s=${country},`, '_blank'); + execute: async context => { + window.open(`https://www.worldtravelguide.net/?s=${context.country}`, '_blank'); }, }); -export type CountryContext = string; +export interface CountryContext { + country: string; +} -export const viewInMapsAction = createAction({ - type: VIEW_IN_MAPS_ACTION, +export const viewInMapsAction = createAction({ + type: ACTION_VIEW_IN_MAPS, getIconType: () => 'popout', getDisplayName: () => 'View in maps', - execute: async country => { - window.open(`https://www.google.com/maps/place/${country}`, '_blank'); + execute: async context => { + window.open(`https://www.google.com/maps/place/${context.country}`, '_blank'); }, }); @@ -100,11 +104,8 @@ function EditUserModal({ } export const createEditUserAction = (getOpenModal: () => Promise) => - createAction<{ - user: User; - update: (user: User) => void; - }>({ - type: EDIT_USER_ACTION, + createAction({ + type: ACTION_EDIT_USER, getIconType: () => 'pencil', getDisplayName: () => 'Edit user', execute: async ({ user, update }) => { @@ -120,8 +121,8 @@ export interface UserContext { } export const createPhoneUserAction = (getUiActionsApi: () => Promise) => - createAction({ - type: PHONE_USER_ACTION, + createAction({ + type: ACTION_PHONE_USER, getDisplayName: () => 'Call phone number', isCompatible: async ({ user }) => user.phone !== undefined, execute: async ({ user }) => { @@ -133,7 +134,7 @@ export const createPhoneUserAction = (getUiActionsApi: () => Promise { uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, undefined)} + onClick={() => uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} > Say hello world! @@ -76,8 +76,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { { - const dynamicAction = createAction<{}>({ - type: `${HELLO_WORLD_ACTION_TYPE}-${name}`, + const dynamicAction = createAction({ + id: `${ACTION_HELLO_WORLD}-${name}`, + type: ACTION_HELLO_WORLD, getDisplayName: () => `Say hello to ${name}`, execute: async () => { const overlay = openModal( @@ -95,7 +96,7 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { }, }); uiActionsApi.registerAction(dynamicAction); - uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction.type); + uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); setConfirmationText( `You've successfully added a new action: ${dynamicAction.getDisplayName( {} diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index fecada71099e88..f1895905a45e16 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -27,17 +27,17 @@ import { lookUpWeatherAction, viewInMapsAction, createEditUserAction, - CALL_PHONE_NUMBER_ACTION, - VIEW_IN_MAPS_ACTION, - TRAVEL_GUIDE_ACTION, - PHONE_USER_ACTION, - EDIT_USER_ACTION, makePhoneCallAction, showcasePluggability, - SHOWCASE_PLUGGABILITY_ACTION, UserContext, CountryContext, PhoneContext, + ACTION_EDIT_USER, + ACTION_SHOWCASE_PLUGGABILITY, + ACTION_CALL_PHONE_NUMBER, + ACTION_TRAVEL_GUIDE, + ACTION_VIEW_IN_MAPS, + ACTION_PHONE_USER, } from './actions/actions'; interface StartDeps { @@ -54,6 +54,15 @@ declare module '../../../src/plugins/ui_actions/public' { [COUNTRY_TRIGGER]: CountryContext; [PHONE_TRIGGER]: PhoneContext; } + + export interface ActionContextMapping { + [ACTION_EDIT_USER]: UserContext; + [ACTION_SHOWCASE_PLUGGABILITY]: {}; + [ACTION_CALL_PHONE_NUMBER]: PhoneContext; + [ACTION_TRAVEL_GUIDE]: CountryContext; + [ACTION_VIEW_IN_MAPS]: CountryContext; + [ACTION_PHONE_USER]: UserContext; + } } export class UiActionsExplorerPlugin implements Plugin { @@ -67,29 +76,24 @@ export class UiActionsExplorerPlugin implements Plugin (await startServices)[1].uiActions) ); - deps.uiActions.registerAction( + deps.uiActions.attachAction( + USER_TRIGGER, createEditUserAction(async () => (await startServices)[0].overlays.openModal) ); - deps.uiActions.attachAction(USER_TRIGGER, PHONE_USER_ACTION); - deps.uiActions.attachAction(USER_TRIGGER, EDIT_USER_ACTION); - // What's missing here is type analysis to ensure the context emitted by the trigger - // is the same context that the action requires. - deps.uiActions.attachAction(COUNTRY_TRIGGER, VIEW_IN_MAPS_ACTION); - deps.uiActions.attachAction(COUNTRY_TRIGGER, TRAVEL_GUIDE_ACTION); - deps.uiActions.attachAction(COUNTRY_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); - deps.uiActions.attachAction(PHONE_TRIGGER, CALL_PHONE_NUMBER_ACTION); - deps.uiActions.attachAction(PHONE_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); - deps.uiActions.attachAction(USER_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); + deps.uiActions.attachAction(COUNTRY_TRIGGER, viewInMapsAction); + deps.uiActions.attachAction(COUNTRY_TRIGGER, lookUpWeatherAction); + deps.uiActions.attachAction(COUNTRY_TRIGGER, showcasePluggability); + deps.uiActions.attachAction(PHONE_TRIGGER, makePhoneCallAction); + deps.uiActions.attachAction(PHONE_TRIGGER, showcasePluggability); + deps.uiActions.attachAction(USER_TRIGGER, showcasePluggability); core.application.register({ id: 'uiActionsExplorer', diff --git a/examples/ui_actions_explorer/public/trigger_context_example.tsx b/examples/ui_actions_explorer/public/trigger_context_example.tsx index 00d974e9381384..4b886521039663 100644 --- a/examples/ui_actions_explorer/public/trigger_context_example.tsx +++ b/examples/ui_actions_explorer/public/trigger_context_example.tsx @@ -47,7 +47,7 @@ const createRowData = ( { - uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, user.countryOfResidence); + uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, { country: user.countryOfResidence }); }} > {user.countryOfResidence} @@ -59,7 +59,7 @@ const createRowData = ( { - uiActionsApi.executeTriggerActions(PHONE_TRIGGER, user.phone!); + uiActionsApi.executeTriggerActions(PHONE_TRIGGER, { phone: user.phone! }); }} > {user.phone} diff --git a/examples/url_generators_examples/README.md b/examples/url_generators_examples/README.md new file mode 100644 index 00000000000000..facd5c90c8c96f --- /dev/null +++ b/examples/url_generators_examples/README.md @@ -0,0 +1,7 @@ +## Access links examples + +This example app shows how to: + - Register a direct access link generator. + - Handle migration of legacy generators into a new one. + +To run this example, use the command `yarn start --run-examples`. Navigate to the access links explorer app \ No newline at end of file diff --git a/examples/url_generators_examples/kibana.json b/examples/url_generators_examples/kibana.json new file mode 100644 index 00000000000000..0767018e3bb981 --- /dev/null +++ b/examples/url_generators_examples/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "urlGeneratorsExamples", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["url_generators_examples"], + "server": false, + "ui": true, + "requiredPlugins": ["share"], + "optionalPlugins": [] +} diff --git a/examples/url_generators_examples/package.json b/examples/url_generators_examples/package.json new file mode 100644 index 00000000000000..e07482db25f435 --- /dev/null +++ b/examples/url_generators_examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "url_generators_examples", + "version": "1.0.0", + "main": "target/examples/url_generators_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.5.3" + } +} diff --git a/examples/url_generators_examples/public/app.tsx b/examples/url_generators_examples/public/app.tsx new file mode 100644 index 00000000000000..c39cd876ea9b15 --- /dev/null +++ b/examples/url_generators_examples/public/app.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { EuiPageBody } from '@elastic/eui'; +import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; +import { Route, Switch, Redirect, Router, useLocation } from 'react-router-dom'; +import { createBrowserHistory } from 'history'; +import { EuiText } from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; + +function useQuery() { + const { search } = useLocation(); + const params = React.useMemo(() => new URLSearchParams(search), [search]); + return params; +} + +interface HelloPageProps { + firstName: string; + lastName: string; +} + +const HelloPage = ({ firstName, lastName }: HelloPageProps) => ( + {`Hello ${firstName} ${lastName}`} +); + +export const Routes: React.FC<{}> = () => { + const query = useQuery(); + + return ( + + + + + + + + + + + + + ); +}; + +export const LinksExample: React.FC<{ + appBasePath: string; +}> = props => { + const history = React.useMemo( + () => + createBrowserHistory({ + basename: props.appBasePath, + }), + [props.appBasePath] + ); + return ( + + + + ); +}; + +export const renderApp = (props: { appBasePath: string }, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/typings/normalize_path/index.d.ts b/examples/url_generators_examples/public/index.ts similarity index 85% rename from typings/normalize_path/index.d.ts rename to examples/url_generators_examples/public/index.ts index 31e064ca63d903..e87f9237bff385 100644 --- a/typings/normalize_path/index.d.ts +++ b/examples/url_generators_examples/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -declare function NormalizePath(path: string, stripTrailing?: boolean): string; +import { AccessLinksExamplesPlugin } from './plugin'; -declare module 'normalize-path' { - export = NormalizePath; -} +export const plugin = () => new AccessLinksExamplesPlugin(); diff --git a/examples/url_generators_examples/public/plugin.tsx b/examples/url_generators_examples/public/plugin.tsx new file mode 100644 index 00000000000000..016494037ec055 --- /dev/null +++ b/examples/url_generators_examples/public/plugin.tsx @@ -0,0 +1,76 @@ +/* + * 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 { SharePluginStart, SharePluginSetup } from '../../../src/plugins/share/public'; +import { Plugin, CoreSetup, AppMountParameters } from '../../../src/core/public'; +import { + HelloLinkGeneratorState, + createHelloPageLinkGenerator, + LegacyHelloLinkGeneratorState, + HELLO_URL_GENERATOR_V1, + HELLO_URL_GENERATOR, + helloPageLinkGeneratorV1, +} from './url_generator'; + +declare module '../../../src/plugins/share/public' { + export interface UrlGeneratorStateMapping { + [HELLO_URL_GENERATOR_V1]: LegacyHelloLinkGeneratorState; + [HELLO_URL_GENERATOR]: HelloLinkGeneratorState; + } +} + +interface StartDeps { + share: SharePluginStart; +} + +interface SetupDeps { + share: SharePluginSetup; +} + +const APP_ID = 'urlGeneratorsExamples'; + +export class AccessLinksExamplesPlugin implements Plugin { + public setup(core: CoreSetup, { share: { urlGenerators } }: SetupDeps) { + urlGenerators.registerUrlGenerator( + createHelloPageLinkGenerator(async () => ({ + appBasePath: (await core.getStartServices())[0].application.getUrlForApp(APP_ID), + })) + ); + + urlGenerators.registerUrlGenerator(helloPageLinkGeneratorV1); + + core.application.register({ + id: APP_ID, + title: 'Access links examples', + async mount(params: AppMountParameters) { + const { renderApp } = await import('./app'); + return renderApp( + { + appBasePath: params.appBasePath, + }, + params + ); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/url_generators_examples/public/url_generator.ts b/examples/url_generators_examples/public/url_generator.ts new file mode 100644 index 00000000000000..f21b1c9295e660 --- /dev/null +++ b/examples/url_generators_examples/public/url_generator.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 url from 'url'; +import { UrlGeneratorState, UrlGeneratorsDefinition } from '../../../src/plugins/share/public'; + +/** + * The name of the latest variable can always stay the same so code that + * uses this link generator statically will switch to the latest version. + * Typescript will warn the developer if incorrect state is being passed + * down. + */ +export const HELLO_URL_GENERATOR = 'HELLO_URL_GENERATOR_V2'; + +export interface HelloLinkState { + firstName: string; + lastName: string; +} + +export type HelloLinkGeneratorState = UrlGeneratorState; + +export const createHelloPageLinkGenerator = ( + getStartServices: () => Promise<{ appBasePath: string }> +): UrlGeneratorsDefinition => ({ + id: HELLO_URL_GENERATOR, + createUrl: async state => { + const startServices = await getStartServices(); + const appBasePath = startServices.appBasePath; + const parsedUrl = url.parse(window.location.href); + + return url.format({ + protocol: parsedUrl.protocol, + host: parsedUrl.host, + pathname: `${appBasePath}/hello`, + query: { + ...state, + }, + }); + }, +}); + +/** + * The name of this legacy generator id changes, but the *value* stays the same. + */ +export const HELLO_URL_GENERATOR_V1 = 'HELLO_URL_GENERATOR'; + +export interface HelloLinkStateV1 { + name: string; +} + +export type LegacyHelloLinkGeneratorState = UrlGeneratorState< + HelloLinkStateV1, + typeof HELLO_URL_GENERATOR, + HelloLinkState +>; + +export const helloPageLinkGeneratorV1: UrlGeneratorsDefinition = { + id: HELLO_URL_GENERATOR_V1, + isDeprecated: true, + migrate: async state => { + return { id: HELLO_URL_GENERATOR, state: { firstName: state.name, lastName: '' } }; + }, +}; diff --git a/examples/url_generators_examples/tsconfig.json b/examples/url_generators_examples/tsconfig.json new file mode 100644 index 00000000000000..091130487791bd --- /dev/null +++ b/examples/url_generators_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/examples/url_generators_explorer/README.md b/examples/url_generators_explorer/README.md new file mode 100644 index 00000000000000..922cf37aff8471 --- /dev/null +++ b/examples/url_generators_explorer/README.md @@ -0,0 +1,8 @@ +## Access links explorer + +This example app shows how to: + - Generate links to other applications + - Generate dynamic links, when the target application is not known + - Handle backward compatibility of urls + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/url_generators_explorer/kibana.json b/examples/url_generators_explorer/kibana.json new file mode 100644 index 00000000000000..94ab75b3388898 --- /dev/null +++ b/examples/url_generators_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "urlGeneratorsExplorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["url_generators_explorer"], + "server": false, + "ui": true, + "requiredPlugins": ["share", "urlGeneratorsExamples"], + "optionalPlugins": [] +} diff --git a/examples/url_generators_explorer/package.json b/examples/url_generators_explorer/package.json new file mode 100644 index 00000000000000..52da533dc0c055 --- /dev/null +++ b/examples/url_generators_explorer/package.json @@ -0,0 +1,17 @@ +{ + "name": "url_generators_explorer", + "version": "1.0.0", + "main": "target/examples/url_generators_explorer", + "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/examples/url_generators_explorer/public/app.tsx b/examples/url_generators_explorer/public/app.tsx new file mode 100644 index 00000000000000..77e804ae08c5f4 --- /dev/null +++ b/examples/url_generators_explorer/public/app.tsx @@ -0,0 +1,170 @@ +/* + * 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, { useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; + +import { EuiPage } from '@elastic/eui'; + +import { EuiButton } from '@elastic/eui'; +import { EuiPageBody } from '@elastic/eui'; +import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { UrlGeneratorsService } from '../../../src/plugins/share/public'; +import { + HELLO_URL_GENERATOR, + HELLO_URL_GENERATOR_V1, +} from '../../url_generators_examples/public/url_generator'; + +interface Props { + getLinkGenerator: UrlGeneratorsService['getUrlGenerator']; +} + +interface MigratedLink { + isDeprecated: boolean; + linkText: string; + link: string; +} + +const ActionsExplorer = ({ getLinkGenerator }: Props) => { + const [migratedLinks, setMigratedLinks] = useState([] as MigratedLink[]); + const [buildingLinks, setBuildingLinks] = useState(false); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + /** + * Lets pretend we grabbed these links from a persistent store, like a saved object. + * Some of these links were created with older versions of the hello link generator. + * They use deprecated generator ids. + */ + const [persistedLinks, setPersistedLinks] = useState([ + { + id: HELLO_URL_GENERATOR_V1, + linkText: 'Say hello to Mary', + state: { + name: 'Mary', + }, + }, + { + id: HELLO_URL_GENERATOR, + linkText: 'Say hello to George', + state: { + firstName: 'George', + lastName: 'Washington', + }, + }, + ]); + + useEffect(() => { + setBuildingLinks(true); + + const updateLinks = async () => { + const updatedLinks = await Promise.all( + persistedLinks.map(async savedLink => { + const generator = getLinkGenerator(savedLink.id); + const link = await generator.createUrl(savedLink.state); + return { + isDeprecated: generator.isDeprecated, + linkText: savedLink.linkText, + link, + }; + }) + ); + setMigratedLinks(updatedLinks); + setBuildingLinks(false); + }; + + updateLinks(); + }, [getLinkGenerator, persistedLinks]); + + return ( + + + Access links explorer + + + +

Create new links using the most recent version of a url generator.

+
+ { + setFirstName(e.target.value); + }} + /> + setLastName(e.target.value)} /> + + setPersistedLinks([ + ...persistedLinks, + { + id: HELLO_URL_GENERATOR, + state: { firstName, lastName }, + linkText: `Say hello to ${firstName} ${lastName}`, + }, + ]) + } + > + Add new link + + + + +

+ Existing links retrieved from storage. The links that were generated from legacy + generators are in red. This can be useful for developers to know they will have to + migrate persisted state or in a future version of Kibana, these links may no longer + work. They still work now because legacy url generators must provide a state + migration function. +

+
+ {buildingLinks ? ( +
loading...
+ ) : ( + migratedLinks.map(link => ( + + + {link.linkText} + +
+
+ )) + )} +
+
+
+
+ ); +}; + +export const renderApp = (props: Props, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_mocks.ts b/examples/url_generators_explorer/public/index.ts similarity index 83% rename from src/legacy/core_plugins/visualizations/public/legacy_mocks.ts rename to examples/url_generators_explorer/public/index.ts index 6cd57bb88bc261..30ff481dbe3a5f 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_mocks.ts +++ b/examples/url_generators_explorer/public/index.ts @@ -17,5 +17,6 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { searchSourceMock } from '../../../../plugins/data/public/search/search_source/mocks'; +import { AccessLinksExplorerPlugin } from './plugin'; + +export const plugin = () => new AccessLinksExplorerPlugin(); diff --git a/examples/url_generators_explorer/public/page.tsx b/examples/url_generators_explorer/public/page.tsx new file mode 100644 index 00000000000000..90bea358048221 --- /dev/null +++ b/examples/url_generators_explorer/public/page.tsx @@ -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 React from 'react'; + +import { + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +interface PageProps { + title: string; + children: React.ReactNode; +} + +export function Page({ title, children }: PageProps) { + return ( + + + + +

{title}

+
+
+
+ + {children} + +
+ ); +} diff --git a/examples/url_generators_explorer/public/plugin.tsx b/examples/url_generators_explorer/public/plugin.tsx new file mode 100644 index 00000000000000..1fe70476b8e79c --- /dev/null +++ b/examples/url_generators_explorer/public/plugin.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 { SharePluginStart } from 'src/plugins/share/public'; +import { Plugin, CoreSetup, AppMountParameters } from '../../../src/core/public'; + +interface StartDeps { + share: SharePluginStart; +} + +export class AccessLinksExplorerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ + id: 'urlGeneratorsExplorer', + title: 'Access links explorer', + async mount(params: AppMountParameters) { + const depsStart = (await core.getStartServices())[1]; + const { renderApp } = await import('./app'); + return renderApp( + { + getLinkGenerator: depsStart.share.urlGenerators.getUrlGenerator, + }, + params + ); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/url_generators_explorer/tsconfig.json b/examples/url_generators_explorer/tsconfig.json new file mode 100644 index 00000000000000..091130487791bd --- /dev/null +++ b/examples/url_generators_explorer/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 c6d858124bc3cd..0be44e740a79f2 100644 --- a/package.json +++ b/package.json @@ -323,6 +323,7 @@ "@types/fetch-mock": "^7.3.1", "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", + "@types/getos": "^3.0.0", "@types/glob": "^7.1.1", "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", @@ -349,6 +350,7 @@ "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", "@types/node-forge": "^0.9.0", + "@types/normalize-path": "^3.0.0", "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", @@ -384,7 +386,7 @@ "@typescript-eslint/parser": "^2.15.0", "angular-mocks": "^1.7.9", "archiver": "^3.1.1", - "axe-core": "^3.3.2", + "axe-core": "^3.4.1", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "babel-plugin-dynamic-import-node": "^2.3.0", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 66fa55479f3b9e..817c4796562e82 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -17,6 +17,7 @@ * under the License. */ +import './legacy/styles.scss'; import { fooLibFn } from '../../foo/public/index'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss new file mode 100644 index 00000000000000..e71a2d485a2f85 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss @@ -0,0 +1,4 @@ +body { + width: $globalStyleConstant; + background-image: url("ui/icon.svg"); +} diff --git a/src/cli/dev_ssl.js b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/async_import.ts similarity index 79% rename from src/cli/dev_ssl.js rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/async_import.ts index 110f44ee57b7d7..9a51937cbac1e4 100644 --- a/src/cli/dev_ssl.js +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/async_import.ts @@ -17,6 +17,4 @@ * under the License. */ -import { resolve } from 'path'; -export const DEV_SSL_CERT_PATH = resolve(__dirname, '../../test/dev_certs/server.crt'); -export const DEV_SSL_KEY_PATH = resolve(__dirname, '../../test/dev_certs/server.key'); +export function foo() {} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts index 9d3871df247397..1ba0b696811525 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts @@ -19,3 +19,7 @@ export * from './lib'; export * from './ext'; + +export async function getFoo() { + return await import('./async_import'); +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg new file mode 100644 index 00000000000000..ae7d5b958bbadc --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss new file mode 100644 index 00000000000000..83995ca65211bd --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss @@ -0,0 +1 @@ +$globalStyleConstant: 10; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 706f79978beee4..d52d89eebe2f13 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -1,557 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`builds expected bundles, saves bundle counts to metadata: 1 async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,exports,__webpack_require__){\\"use strict\\";Object.defineProperty(exports,\\"__esModule\\",{value:true});exports.foo=foo;function foo(){}}}]);"`; + exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` OptimizerConfig { "bundles": Array [ Bundle { "cache": BundleCache { - "path": /plugins/bar/target/public/.kbn-optimizer-cache, + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, }, - "contextDir": /plugins/bar, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, "entry": "./public/index", "id": "bar", - "outputDir": /plugins/bar/target/public, - "sourceRoot": , + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, Bundle { "cache": BundleCache { - "path": /plugins/foo/target/public/.kbn-optimizer-cache, + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, }, - "contextDir": /plugins/foo, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "entry": "./public/index", "id": "foo", - "outputDir": /plugins/foo/target/public, - "sourceRoot": , + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, ], "cache": true, - "dist": false, + "dist": true, "inspectWorkers": false, "maxWorkerCount": 1, "plugins": Array [ Object { - "directory": /plugins/bar, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, "id": "bar", "isUiPlugin": true, }, Object { - "directory": /plugins/baz, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/baz, "id": "baz", "isUiPlugin": false, }, Object { - "directory": /plugins/foo, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "id": "foo", "isUiPlugin": true, }, ], "profileWebpack": false, - "repoRoot": , + "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "watch": false, } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = ` -"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/bar\\"] = -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); -/******/ }) -/************************************************************************/ -/******/ ({ - -/***/ \\"../foo/public/ext.ts\\": -/*!****************************!*\\\\ - !*** ../foo/public/ext.ts ***! - \\\\****************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -\\"use strict\\"; - - -Object.defineProperty(exports, \\"__esModule\\", { - value: true -}); -exports.ext = void 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. - */ -const ext = 'TRUE'; -exports.ext = ext; - -/***/ }), - -/***/ \\"../foo/public/index.ts\\": -/*!******************************!*\\\\ - !*** ../foo/public/index.ts ***! - \\\\******************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -\\"use strict\\"; - - -Object.defineProperty(exports, \\"__esModule\\", { - value: true -}); - -var _lib = __webpack_require__(/*! ./lib */ \\"../foo/public/lib.ts\\"); - -Object.keys(_lib).forEach(function (key) { - if (key === \\"default\\" || key === \\"__esModule\\") return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function () { - return _lib[key]; - } - }); -}); - -var _ext = __webpack_require__(/*! ./ext */ \\"../foo/public/ext.ts\\"); - -Object.keys(_ext).forEach(function (key) { - if (key === \\"default\\" || key === \\"__esModule\\") return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function () { - return _ext[key]; - } - }); -}); - -/***/ }), - -/***/ \\"../foo/public/lib.ts\\": -/*!****************************!*\\\\ - !*** ../foo/public/lib.ts ***! - \\\\****************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -\\"use strict\\"; - - -Object.defineProperty(exports, \\"__esModule\\", { - value: true -}); -exports.fooLibFn = fooLibFn; - -/* - * 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. - */ -function fooLibFn() { - return 'foo'; -} - -/***/ }), - -/***/ \\"./public/index.ts\\": -/*!*************************!*\\\\ - !*** ./public/index.ts ***! - \\\\*************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -\\"use strict\\"; - - -Object.defineProperty(exports, \\"__esModule\\", { - value: true -}); -var _exportNames = { - fooLibFn: true -}; -Object.defineProperty(exports, \\"fooLibFn\\", { - enumerable: true, - get: function () { - return _index.fooLibFn; - } -}); - -var _index = __webpack_require__(/*! ../../foo/public/index */ \\"../foo/public/index.ts\\"); - -var _lib = __webpack_require__(/*! ./lib */ \\"./public/lib.ts\\"); - -Object.keys(_lib).forEach(function (key) { - if (key === \\"default\\" || key === \\"__esModule\\") return; - if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function () { - return _lib[key]; - } - }); -}); - -/***/ }), - -/***/ \\"./public/lib.ts\\": -/*!***********************!*\\\\ - !*** ./public/lib.ts ***! - \\\\***********************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -\\"use strict\\"; - - -Object.defineProperty(exports, \\"__esModule\\", { - value: true -}); -exports.barLibFn = barLibFn; - -/* - * 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. - */ -function barLibFn() { - return 'bar'; -} - -/***/ }) - -/******/ })[\\"plugin\\"]; -//# sourceMappingURL=bar.plugin.js.map" -`; - -exports[`builds expected bundles, saves bundle counts to metadata: foo bundle 1`] = ` -"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/foo\\"] = -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); -/******/ }) -/************************************************************************/ -/******/ ({ - -/***/ \\"./public/ext.ts\\": -/*!***********************!*\\\\ - !*** ./public/ext.ts ***! - \\\\***********************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -\\"use strict\\"; - - -Object.defineProperty(exports, \\"__esModule\\", { - value: true -}); -exports.ext = void 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. - */ -const ext = 'TRUE'; -exports.ext = ext; - -/***/ }), - -/***/ \\"./public/index.ts\\": -/*!*************************!*\\\\ - !*** ./public/index.ts ***! - \\\\*************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { +exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { await del(TMP_DIR); @@ -51,20 +51,25 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], maxWorkerCount: 1, + dist: true, }); expect(config).toMatchSnapshot('OptimizerConfig'); - const msgs = await runOptimizer(config) - .pipe( - tap(state => { - if (state.event?.type === 'worker stdio') { - // eslint-disable-next-line no-console - console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + const log = new ToolingLog({ + level: 'error', + writeTo: { + write(chunk) { + if (chunk.endsWith('\n')) { + chunk = chunk.slice(0, -1); } - }), - toArray() - ) + // eslint-disable-next-line no-console + console.error(chunk); + }, + }, + }); + const msgs = await runOptimizer(config) + .pipe(logOptimizerState(log, config), toArray()) .toPromise(); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { @@ -123,6 +128,10 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/foo.plugin.js'), 'utf8') ).toMatchSnapshot('foo bundle'); + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/1.plugin.js'), 'utf8') + ).toMatchSnapshot('1 async bundle'); + expect( Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/bar/target/public/bar.plugin.js'), 'utf8') ).toMatchSnapshot('bar bundle'); @@ -130,26 +139,36 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const foo = config.bundles.find(b => b.id === 'foo')!; expect(foo).toBeTruthy(); foo.cache.refresh(); - expect(foo.cache.getModuleCount()).toBe(3); + expect(foo.cache.getModuleCount()).toBe(4); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /plugins/foo/public/ext.ts, - /plugins/foo/public/index.ts, - /plugins/foo/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, ] `); const bar = config.bundles.find(b => b.id === 'bar')!; expect(bar).toBeTruthy(); bar.cache.refresh(); - expect(bar.cache.getModuleCount()).toBe(5); + expect(bar.cache.getModuleCount()).toBe( + // code + styles + style/css-loader runtimes + 15 + ); + expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /plugins/foo/public/ext.ts, - /plugins/foo/public/index.ts, - /plugins/foo/public/lib.ts, - /plugins/bar/public/index.ts, - /plugins/bar/public/lib.ts, + /node_modules/css-loader/package.json, + /node_modules/style-loader/package.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, ] `); }); @@ -159,6 +178,7 @@ it('uses cache on second run and exist cleanly', async () => { repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], maxWorkerCount: 1, + dist: true, }); const msgs = await runOptimizer(config) diff --git a/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap new file mode 100644 index 00000000000000..2973ac116d6bd1 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseDirPath() parses / 1`] = ` +Object { + "dirs": Array [], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo 1`] = ` +Object { + "dirs": Array [ + "foo", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo/bar/baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo/bar/baz/ 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses c:\\ 1`] = ` +Object { + "dirs": Array [], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo 1`] = ` +Object { + "dirs": Array [ + "foo", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo\\bar\\baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo\\bar\\baz\\ 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseFilePath() parses /foo 1`] = ` +Object { + "dirs": Array [], + "filename": "foo", + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz", + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "/", +} +`; + +exports[`parseFilePath() parses c:/foo/bar/baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo 1`] = ` +Object { + "dirs": Array [], + "filename": "foo", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "c:", +} +`; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.mocks.ts b/packages/kbn-optimizer/src/worker/parse_path.test.ts similarity index 57% rename from src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.mocks.ts rename to packages/kbn-optimizer/src/worker/parse_path.test.ts index a0b9d7c779b029..72197e8c8fb07a 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.mocks.ts +++ b/packages/kbn-optimizer/src/worker/parse_path.test.ts @@ -17,26 +17,20 @@ * under the License. */ -import { - notificationServiceMock, - overlayServiceMock, - httpServiceMock, - injectedMetadataServiceMock, -} from '../../../../../../../core/public/mocks'; +import { parseFilePath, parseDirPath } from './parse_path'; -jest.doMock('ui/new_platform', () => { - return { - npSetup: { - core: { - notifications: notificationServiceMock.createSetupContract(), - }, - }, - npStart: { - core: { - overlays: overlayServiceMock.createStartContract(), - http: httpServiceMock.createStartContract({ basePath: 'path' }), - injectedMetadata: injectedMetadataServiceMock.createStartContract(), - }, - }, - }; +const DIRS = ['/', '/foo/bar/baz/', 'c:\\', 'c:\\foo\\bar\\baz\\']; +const AMBIGUOUS = ['/foo', '/foo/bar/baz', 'c:\\foo', 'c:\\foo\\bar\\baz']; +const FILES = ['/foo/bar/baz.json', 'c:/foo/bar/baz.json', 'c:\\foo\\bar\\baz.json']; + +describe('parseFilePath()', () => { + it.each([...FILES, ...AMBIGUOUS])('parses %s', path => { + expect(parseFilePath(path)).toMatchSnapshot(); + }); +}); + +describe('parseDirPath()', () => { + it.each([...DIRS, ...AMBIGUOUS])('parses %s', path => { + expect(parseDirPath(path)).toMatchSnapshot(); + }); }); diff --git a/packages/kbn-optimizer/src/worker/parse_path.ts b/packages/kbn-optimizer/src/worker/parse_path.ts new file mode 100644 index 00000000000000..88152df55b84f4 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/parse_path.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import normalizePath from 'normalize-path'; + +/** + * Parse an absolute path, supporting normalized paths from webpack, + * into a list of directories and root + */ +export function parseDirPath(path: string) { + const filePath = parseFilePath(path); + return { + ...filePath, + dirs: [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], + filename: undefined, + }; +} + +export function parseFilePath(path: string) { + const normalized = normalizePath(path); + const [root, ...others] = normalized.split('/'); + return { + root: root === '' ? '/' : root, + dirs: others.slice(0, -1), + filename: others[others.length - 1] || undefined, + }; +} diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 7dcce8a0fae8d8..e87ddc7d0185cf 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -27,9 +27,10 @@ import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig } from '../common'; +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, ascending } from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; +import { parseFilePath } from './parse_path'; import { isExternalModule, isNormalModule, @@ -108,26 +109,25 @@ const observeCompiler = ( for (const module of normalModules) { const path = getModulePath(module); + const parsedPath = parseFilePath(path); - const parsedPath = Path.parse(path); - const dirSegments = parsedPath.dir.split(Path.sep); - if (!dirSegments.includes('node_modules')) { + if (!parsedPath.dirs.includes('node_modules')) { referencedFiles.add(path); continue; } - const nmIndex = dirSegments.lastIndexOf('node_modules'); - const isScoped = dirSegments[nmIndex + 1].startsWith('@'); + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); referencedFiles.add( Path.join( parsedPath.root, - ...dirSegments.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), 'package.json' ) ); } - const files = Array.from(referencedFiles); + const files = Array.from(referencedFiles).sort(ascending(p => p)); const mtimes = new Map( files.map((path): [string, number | undefined] => { try { diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3c6ae78bc4d911..5d8ef7626f6305 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -30,6 +30,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import * as SharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig } from '../common'; +import { parseDirPath } from './parse_path'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -135,7 +136,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { } // manually force ui/* urls in legacy styles to resolve to ui/legacy/public - if (uri.startsWith('ui/') && base.split(Path.sep).includes('legacy')) { + if (uri.startsWith('ui/') && parseDirPath(base).dirs.includes('legacy')) { return Path.resolve( worker.repoRoot, 'src/legacy/ui/public', @@ -150,7 +151,9 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { { loader: 'sass-loader', options: { - sourceMap: !worker.dist, + // must always be enabled as long as we're using the `resolve-url-loader` to + // rewrite `ui/*` urls. They're dropped by subsequent loaders though + sourceMap: true, prependData(loaderContext: webpack.loader.LoaderContext) { return `@import ${stringifyRequest( loaderContext, diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 771bf43c4020a4..d7d4dc14519c3d 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -102,6 +102,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug 'start', '--optimize.enabled=false', '--logging.json=false', + '--logging.verbose=true', '--migrations.skip=true', ], cwd: generatedPath, diff --git a/packages/kbn-storybook/storybook_config/config.js b/packages/kbn-storybook/storybook_config/config.js index a7975773e675be..d97bd3f7c2dccc 100644 --- a/packages/kbn-storybook/storybook_config/config.js +++ b/packages/kbn-storybook/storybook_config/config.js @@ -55,7 +55,7 @@ addParameters({ brandTitle: 'Kibana Storybook', brandUrl: 'https://github.com/elastic/kibana/tree/master/packages/kbn-storybook', }), - showPanel: true, + showPanel: false, isFullscreen: false, panelPosition: 'bottom', isToolshown: true, diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/packages/kbn-test/src/ci_parallel_process_prefix.ts similarity index 66% rename from src/legacy/core_plugins/kibana/public/home/index.ts rename to packages/kbn-test/src/ci_parallel_process_prefix.ts index 74b6da33c65422..67dafc7e853242 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/packages/kbn-test/src/ci_parallel_process_prefix.ts @@ -17,13 +17,14 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { HomePlugin } from './plugin'; +const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; +const num = process.env.CI_PARALLEL_PROCESS_NUMBER + ? `worker-${process.env.CI_PARALLEL_PROCESS_NUMBER}-` + : ''; -const instance = new HomePlugin({ - env: npSetup.plugins.kibanaLegacy.env, -} as PluginInitializerContext); -instance.setup(npSetup.core, npSetup.plugins); - -instance.start(npStart.core, npStart.plugins); +/** + * A prefix string that is unique for each parallel CI process that + * is an empty string outside of CI, so it can be safely injected + * into strings as a prefix + */ +export const CI_PARALLEL_PROCESS_PREFIX = `${job}${num}`; diff --git a/packages/kbn-test/src/failed_tests_reporter/github_api.ts b/packages/kbn-test/src/failed_tests_reporter/github_api.ts index d8a952bee42e59..7da79b5b67e63f 100644 --- a/packages/kbn-test/src/failed_tests_reporter/github_api.ts +++ b/packages/kbn-test/src/failed_tests_reporter/github_api.ts @@ -33,6 +33,15 @@ export interface GithubIssue { body: string; } +/** + * Minimal GithubIssue type that can be easily replicated by dry-run helpers + */ +export interface GithubIssueMini { + number: GithubIssue['number']; + body: GithubIssue['body']; + html_url: GithubIssue['html_url']; +} + type RequestOptions = AxiosRequestConfig & { safeForDryRun?: boolean; maxAttempts?: number; @@ -162,7 +171,7 @@ export class GithubApi { } async createIssue(title: string, body: string, labels?: string[]) { - const resp = await this.request( + const resp = await this.request( { method: 'POST', url: Url.resolve(BASE_URL, 'issues'), @@ -173,11 +182,13 @@ export class GithubApi { }, }, { + body, + number: 999, html_url: 'https://dryrun', } ); - return resp.data.html_url; + return resp.data; } private async request( diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts index ef6ab3c51ab194..5bbc72fe04e86e 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts @@ -78,9 +78,7 @@ describe('updateFailureIssue()', () => { 'https://build-url', { html_url: 'https://github.com/issues/1234', - labels: ['some-label'], number: 1234, - title: 'issue title', body: dedent` # existing issue body diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts index 97e9d517576fc6..1413d054984594 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts @@ -18,7 +18,7 @@ */ import { TestFailure } from './get_failures'; -import { GithubIssue, GithubApi } from './github_api'; +import { GithubIssueMini, GithubApi } from './github_api'; import { getIssueMetadata, updateIssueMetadata } from './issue_metadata'; export async function createFailureIssue(buildUrl: string, failure: TestFailure, api: GithubApi) { @@ -44,7 +44,7 @@ export async function createFailureIssue(buildUrl: string, failure: TestFailure, return await api.createIssue(title, body, ['failed-test']); } -export async function updateFailureIssue(buildUrl: string, issue: GithubIssue, api: GithubApi) { +export async function updateFailureIssue(buildUrl: string, issue: GithubIssueMini, api: GithubApi) { // Increment failCount const newCount = getIssueMetadata(issue.body, 'test.failCount', 0) + 1; const newBody = updateIssueMetadata(issue.body, { diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index fc52fa6cbf9e7d..9324f9eb42aa5f 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -20,8 +20,8 @@ import { REPO_ROOT, run, createFailError, createFlagError } from '@kbn/dev-utils'; import globby from 'globby'; -import { getFailures } from './get_failures'; -import { GithubApi } from './github_api'; +import { getFailures, TestFailure } from './get_failures'; +import { GithubApi, GithubIssueMini } from './github_api'; import { updateFailureIssue, createFailureIssue } from './report_failure'; import { getIssueMetadata } from './issue_metadata'; import { readTestReport } from './test_report'; @@ -73,6 +73,11 @@ export function runFailedTestsReporterCli() { absolute: true, }); + const newlyCreatedIssues: Array<{ + failure: TestFailure; + newIssue: GithubIssueMini; + }> = []; + for (const reportPath of reportPaths) { const report = await readTestReport(reportPath); const messages = Array.from(getReportMessageIter(report)); @@ -94,12 +99,22 @@ export function runFailedTestsReporterCli() { continue; } - const existingIssue = await githubApi.findFailedTestIssue( + let existingIssue: GithubIssueMini | undefined = await githubApi.findFailedTestIssue( i => getIssueMetadata(i.body, 'test.class') === failure.classname && getIssueMetadata(i.body, 'test.name') === failure.name ); + if (!existingIssue) { + const newlyCreated = newlyCreatedIssues.find( + ({ failure: f }) => f.classname === failure.classname && f.name === failure.name + ); + + if (newlyCreated) { + existingIssue = newlyCreated.newIssue; + } + } + if (existingIssue) { const newFailureCount = await updateFailureIssue(buildUrl, existingIssue, githubApi); const url = existingIssue.html_url; @@ -110,11 +125,12 @@ export function runFailedTestsReporterCli() { continue; } - const newIssueUrl = await createFailureIssue(buildUrl, failure, githubApi); + const newIssue = await createFailureIssue(buildUrl, failure, githubApi); pushMessage('Test has not failed recently on tracked branches'); if (updateGithub) { - pushMessage(`Created new issue: ${newIssueUrl}`); + pushMessage(`Created new issue: ${newIssue.html_url}`); } + newlyCreatedIssues.push({ failure, newIssue }); } // mutates report to include messages and writes updated report to disk diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js index 136704a639bff8..5f58190078f0d2 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js @@ -42,10 +42,11 @@ export async function runElasticsearch({ config, options }) { esFrom: esFrom || config.get('esTestCluster.from'), dataArchive: config.get('esTestCluster.dataArchive'), esArgs, + esEnvVars, ssl, }); - await cluster.start(esArgs, esEnvVars); + await cluster.start(); if (isSecurityEnabled) { await setupUsers({ diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/junit_report_path.ts index 11eaf3d2b14a5e..90405d7a89c024 100644 --- a/packages/kbn-test/src/junit_report_path.ts +++ b/packages/kbn-test/src/junit_report_path.ts @@ -18,15 +18,13 @@ */ import { resolve } from 'path'; - -const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; -const num = process.env.CI_WORKER_NUMBER ? `worker-${process.env.CI_WORKER_NUMBER}-` : ''; +import { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export function makeJunitReportPath(rootDirectory: string, reportName: string) { return resolve( rootDirectory, 'target/junit', process.env.JOB || '.', - `TEST-${job}${num}${reportName}.xml` + `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}.xml` ); } diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index 355304c86a3c34..f795b32d78b8e8 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,6 +22,7 @@ import { format } from 'url'; import { get } from 'lodash'; import toPath from 'lodash/internal/toPath'; import { Cluster } from '@kbn/es'; +import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; import { esTestConfig } from './es_test_config'; import { KIBANA_ROOT } from '../'; @@ -38,14 +39,22 @@ export function createLegacyEsTestCluster(options = {}) { basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), dataArchive, - esArgs, + esArgs: customEsArgs = [], + esEnvVars, + clusterName: customClusterName = 'es-test-cluster', ssl, } = options; - const randomHash = Math.random() - .toString(36) - .substring(2); - const clusterName = `test-${randomHash}`; + const clusterName = `${CI_PARALLEL_PROCESS_PREFIX}${customClusterName}`; + + const esArgs = [ + `cluster.name=${clusterName}`, + `http.port=${port}`, + 'discovery.type=single-node', + `transport.port=${esTestConfig.getTransportPort()}`, + ...customEsArgs, + ]; + const config = { version: esTestConfig.getVersion(), installPath: resolve(basePath, clusterName), @@ -55,7 +64,6 @@ export function createLegacyEsTestCluster(options = {}) { basePath, esArgs, }; - const transportPort = esTestConfig.getTransportPort(); const cluster = new Cluster({ log, ssl }); @@ -67,7 +75,7 @@ export function createLegacyEsTestCluster(options = {}) { return esFrom === 'snapshot' ? 3 * minute : 6 * minute; } - async start(esArgs = [], esEnvVars) { + async start() { let installPath; if (esFrom === 'source') { @@ -86,13 +94,7 @@ export function createLegacyEsTestCluster(options = {}) { await cluster.start(installPath, { password: config.password, - esArgs: [ - `cluster.name=${clusterName}`, - `http.port=${port}`, - 'discovery.type=single-node', - `transport.port=${transportPort}`, - ...esArgs, - ], + esArgs, esEnvVars, }); } diff --git a/renovate.json5 b/renovate.json5 index 58a64a5d0f9679..ca2cd2e6bcd932 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -665,6 +665,14 @@ '@types/nodemailer', ], }, + { + groupSlug: 'normalize-path', + groupName: 'normalize-path related packages', + packageNames: [ + 'normalize-path', + '@types/normalize-path', + ], + }, { groupSlug: 'numeral', groupName: 'numeral related packages', diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index be3fc319389d71..29d0fe16ee126a 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -28,8 +28,6 @@ import { getConfigPath } from '../../core/server/path'; import { bootstrap } from '../../core/server'; import { readKeystore } from './read_keystore'; -import { DEV_SSL_CERT_PATH, DEV_SSL_KEY_PATH } from '../dev_ssl'; - function canRequire(path) { try { require.resolve(path); @@ -90,7 +88,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.ssl) { // @kbn/dev-utils is part of devDependencies - const { CA_CERT_PATH } = require('@kbn/dev-utils'); + const { CA_CERT_PATH, KBN_KEY_PATH, KBN_CERT_PATH } = require('@kbn/dev-utils'); const customElasticsearchHosts = opts.elasticsearch ? opts.elasticsearch.split(',') : [].concat(get('elasticsearch.hosts') || []); @@ -104,6 +102,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { ensureNotDefined('server.ssl.key'); ensureNotDefined('server.ssl.keystore.path'); ensureNotDefined('server.ssl.truststore.path'); + ensureNotDefined('server.ssl.certificateAuthorities'); ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); const elasticsearchHosts = ( @@ -121,10 +120,9 @@ 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('server.ssl.certificate', KBN_CERT_PATH); + set('server.ssl.key', KBN_KEY_PATH); + set('server.ssl.certificateAuthorities', CA_CERT_PATH); set('elasticsearch.hosts', elasticsearchHosts); set('elasticsearch.ssl.certificateAuthorities', CA_CERT_PATH); } diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 2769079757bc38..0f592d108c5617 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -148,8 +148,8 @@ import { MyAppRoot } from './components/app.ts'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ -export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, appBasePath }: AppMountParams) => { - ReactDOM.render(, element); +export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, history }: AppMountParams) => { + ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); } ``` diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 9abc2bb77d7d12..cb38dac0e20ce7 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -453,7 +453,7 @@ describe('Plugin', () => { const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); const unmountMock = jest.fn(); renderAppMock.mockReturnValue(unmountMock); - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); // Grab registered mount function @@ -478,7 +478,7 @@ 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) => { +export const renderApp = ({ element, history }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { // Hide the chrome while this app is mounted for a full screen experience core.chrome.setIsVisible(false); @@ -491,7 +491,7 @@ export const renderApp = ({ element, appBasePath }: AppMountParams, core: CoreSt // Render app ReactDOM.render( - , + , element ); @@ -512,12 +512,14 @@ In testing `renderApp` you should be verifying that: ```typescript /** public/application.test.ts */ +import { createMemoryHistory } from 'history'; +import { ScopedHistory } from 'src/core/public'; 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 params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Verify some expected DOM element is rendered into the element @@ -529,7 +531,7 @@ describe('renderApp', () => { }); it('unsubscribes from uiSettings', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Create a fake Subject you can use to monitor observers const settings$ = new Subject(); @@ -544,7 +546,7 @@ describe('renderApp', () => { }); it('resets chrome visibility', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Verify stateful Core API was called on mount diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index facb818c60ff94..318afb652999ef 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -347,7 +347,6 @@ export interface AppMountParameters { * * export renderApp = ({ element, history }: AppMountParameters) => { * ReactDOM.render( - * // pass `appBasePath` to `basename` * * * , @@ -429,7 +428,7 @@ export interface AppMountParameters { * import { CoreStart, AppMountParams } from 'src/core/public'; * import { MyPluginDepsStart } from './plugin'; * - * export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { + * export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { * const { renderApp, hasUnsavedChanges } = await import('./application'); * onAppLeave(actions => { * if(hasUnsavedChanges()) { @@ -437,7 +436,7 @@ export interface AppMountParameters { * } * return actions.default(); * }); - * return renderApp(params); + * return renderApp({ element, history }); * } * ``` */ diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 3521d7ef9c66eb..9b672d40961d8e 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -120,6 +120,7 @@ export class DocLinksService { }, management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, + dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, }, }, }); diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index efd9fdd0536746..f223956075e979 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -21,6 +21,7 @@ import fetchMock from 'fetch-mock/es5/client'; import { readFileSync } from 'fs'; import { join } from 'path'; +import { first } from 'rxjs/operators'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; @@ -30,9 +31,11 @@ function delay(duration: number) { return new Promise(r => setTimeout(r, duration)); } +const BASE_PATH = 'http://localhost/myBase'; + describe('Fetch', () => { const fetchInstance = new Fetch({ - basePath: new BasePath('http://localhost/myBase'), + basePath: new BasePath(BASE_PATH), kibanaVersion: 'VERSION', }); afterEach(() => { @@ -40,6 +43,79 @@ describe('Fetch', () => { fetchInstance.removeAllInterceptors(); }); + describe('getRequestCount$', () => { + const getCurrentRequestCount = () => + fetchInstance + .getRequestCount$() + .pipe(first()) + .toPromise(); + + it('should increase and decrease when request receives success response', async () => { + fetchMock.get('*', 200); + + const fetchResponse = fetchInstance.fetch('/path'); + expect(await getCurrentRequestCount()).toEqual(1); + + await expect(fetchResponse).resolves.not.toThrow(); + expect(await getCurrentRequestCount()).toEqual(0); + }); + + it('should increase and decrease when request receives error response', async () => { + fetchMock.get('*', 500); + + const fetchResponse = fetchInstance.fetch('/path'); + expect(await getCurrentRequestCount()).toEqual(1); + + await expect(fetchResponse).rejects.toThrow(); + expect(await getCurrentRequestCount()).toEqual(0); + }); + + it('should increase and decrease when request fails', async () => { + fetchMock.get('*', Promise.reject('Network!')); + + const fetchResponse = fetchInstance.fetch('/path'); + expect(await getCurrentRequestCount()).toEqual(1); + + await expect(fetchResponse).rejects.toThrow(); + expect(await getCurrentRequestCount()).toEqual(0); + }); + + it('should change for multiple requests', async () => { + fetchMock.get(`${BASE_PATH}/success`, 200); + fetchMock.get(`${BASE_PATH}/fail`, 400); + fetchMock.get(`${BASE_PATH}/network-fail`, Promise.reject('Network!')); + + const requestCounts: number[] = []; + const subscription = fetchInstance + .getRequestCount$() + .subscribe(count => requestCounts.push(count)); + + const success1 = fetchInstance.fetch('/success'); + const success2 = fetchInstance.fetch('/success'); + const failure1 = fetchInstance.fetch('/fail'); + const failure2 = fetchInstance.fetch('/fail'); + const networkFailure1 = fetchInstance.fetch('/network-fail'); + const success3 = fetchInstance.fetch('/success'); + const failure3 = fetchInstance.fetch('/fail'); + const networkFailure2 = fetchInstance.fetch('/network-fail'); + + const swallowError = (p: Promise) => p.catch(() => {}); + await Promise.all([ + success1, + success2, + success3, + swallowError(failure1), + swallowError(failure2), + swallowError(failure3), + swallowError(networkFailure1), + swallowError(networkFailure2), + ]); + + expect(requestCounts).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + subscription.unsubscribe(); + }); + }); + describe('http requests', () => { it('should fail with invalid arguments', async () => { fetchMock.get('*', {}); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index b433acdb6dbb97..d88dc2e3a90371 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -19,6 +19,7 @@ import { merge } from 'lodash'; import { format } from 'url'; +import { BehaviorSubject } from 'rxjs'; import { IBasePath, @@ -43,6 +44,7 @@ const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; export class Fetch { private readonly interceptors = new Set(); + private readonly requestCount$ = new BehaviorSubject(0); constructor(private readonly params: Params) {} @@ -57,6 +59,10 @@ export class Fetch { this.interceptors.clear(); } + public getRequestCount$() { + return this.requestCount$.asObservable(); + } + public readonly delete = this.shorthand('DELETE'); public readonly get = this.shorthand('GET'); public readonly head = this.shorthand('HEAD'); @@ -76,6 +82,7 @@ export class Fetch { // a halt is called we do not resolve or reject, halting handling of the promise. return new Promise>(async (resolve, reject) => { try { + this.requestCount$.next(this.requestCount$.value + 1); const interceptedOptions = await interceptRequest( optionsWithPath, this.interceptors, @@ -98,6 +105,8 @@ export class Fetch { if (!(error instanceof HttpInterceptHaltError)) { reject(error); } + } finally { + this.requestCount$.next(this.requestCount$.value - 1); } }); }; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index a40fcb06273dd4..78220af9cc83b5 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -24,6 +24,7 @@ import { loadingServiceMock } from './http_service.test.mocks'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { HttpService } from './http_service'; +import { Observable } from 'rxjs'; describe('interceptors', () => { afterEach(() => fetchMock.restore()); @@ -52,6 +53,18 @@ describe('interceptors', () => { }); }); +describe('#setup()', () => { + it('registers Fetch#getLoadingCount$() with LoadingCountSetup#addLoadingCountSource()', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const httpService = new HttpService(); + httpService.setup({ fatalErrors, injectedMetadata }); + const loadingServiceSetup = loadingServiceMock.setup.mock.results[0].value; + // We don't verify that this Observable comes from Fetch#getLoadingCount$() to avoid complex mocking + expect(loadingServiceSetup.addLoadingCountSource).toHaveBeenCalledWith(expect.any(Observable)); + }); +}); + describe('#stop()', () => { it('calls loadingCount.stop()', () => { const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 44fc9d65565d4e..98de1d919c4815 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -45,6 +45,7 @@ export class HttpService implements CoreService { ); const fetchService = new Fetch({ basePath, kibanaVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); + loadingCount.addLoadingCountSource(fetchService.getRequestCount$()); this.service = { basePath, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 8ea672890ca297..c860e9de8334e9 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -16,9 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import { createMemoryHistory } from 'history'; + +// Only import types from '.' to avoid triggering default Jest mocks. +import { CoreContext, PluginInitializerContext, AppMountParameters } from '.'; +// Import values from their individual modules instead. +import { ScopedHistory } from './application'; + import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; -import { CoreContext, PluginInitializerContext } from '.'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from './http/http_service.mock'; @@ -139,10 +145,27 @@ function createStorageMock() { return storageMock; } +function createAppMountParametersMock(appBasePath = '') { + // Assemble an in-memory history mock using the provided basePath + const rawHistory = createMemoryHistory(); + rawHistory.push(appBasePath); + const history = new ScopedHistory(rawHistory, appBasePath); + + const params: jest.Mocked = { + appBasePath, + element: document.createElement('div'), + history, + onAppLeave: jest.fn(), + }; + + return params; +} + export const coreMock = { createCoreContext, createSetup: createCoreSetupMock, createStart: createCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, createStorage: createStorageMock, + createAppMountParamters: createAppMountParametersMock, }; diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index b40dbdc1b66519..a91e128f62d2dc 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -81,6 +81,19 @@ describe('core deprecations', () => { }); }); + describe('xsrfDeprecation', () => { + it('logs a warning if server.xsrf.whitelist is set', () => { + const { messages } = applyCoreDeprecations({ + server: { xsrf: { whitelist: ['/path'] } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. It will be removed in 8.0 release. Instead, supply the \\"kbn-xsrf\\" header.", + ] + `); + }); + }); + describe('rewriteBasePath', () => { it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => { const { messages } = applyCoreDeprecations({ diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 4fa51dcd5a0825..d91e55115d0b13 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -38,6 +38,19 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; +const xsrfDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if ( + has(settings, 'server.xsrf.whitelist') && + get(settings, 'server.xsrf.whitelist').length > 0 + ) { + log( + 'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' + + 'It will be removed in 8.0 release. Instead, supply the "kbn-xsrf" header.' + ); + } + return settings; +}; + const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { log( @@ -177,4 +190,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rewriteBasePathDeprecation, cspRulesDeprecation, mapManifestServiceUrlDeprecation, + xsrfDeprecation, ]; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 022a03e01d37df..2667859f303d44 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -33,6 +33,9 @@ import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; import { duration } from 'moment'; +const delay = async (durationMs: number) => + await new Promise(resolve => setTimeout(resolve, durationMs)); + let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); const deps = { @@ -42,7 +45,7 @@ configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], healthCheck: { - delay: duration(2000), + delay: duration(10), }, ssl: { verificationMode: 'none', @@ -125,21 +128,21 @@ describe('#setup', () => { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` -Object { - "healthCheckDelay": "PT2S", - "hosts": Array [ - "http://8.8.8.8", - ], - "logQueries": true, - "requestHeadersWhitelist": Array [ - undefined, - ], - "ssl": Object { - "certificate": "certificate-value", - "verificationMode": "none", - }, -} -`); + Object { + "healthCheckDelay": "PT0.01S", + "hosts": Array [ + "http://8.8.8.8", + ], + "logQueries": true, + "requestHeadersWhitelist": Array [ + undefined, + ], + "ssl": Object { + "certificate": "certificate-value", + "verificationMode": "none", + }, + } + `); }); it('falls back to elasticsearch config if custom config not passed', async () => { const setupContract = await elasticsearchService.setup(deps); @@ -150,24 +153,24 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` -Object { - "healthCheckDelay": "PT2S", - "hosts": Array [ - "http://1.2.3.4", - ], - "requestHeadersWhitelist": Array [ - undefined, - ], - "ssl": Object { - "alwaysPresentCertificate": undefined, - "certificate": undefined, - "certificateAuthorities": undefined, - "key": undefined, - "keyPassphrase": undefined, - "verificationMode": "none", - }, -} -`); + Object { + "healthCheckDelay": "PT0.01S", + "hosts": Array [ + "http://1.2.3.4", + ], + "requestHeadersWhitelist": Array [ + undefined, + ], + "ssl": Object { + "alwaysPresentCertificate": undefined, + "certificate": undefined, + "certificateAuthorities": undefined, + "key": undefined, + "keyPassphrase": undefined, + "verificationMode": "none", + }, + } + `); }); it('does not merge elasticsearch hosts if custom config overrides', async () => { @@ -213,6 +216,45 @@ Object { `); }); }); + + it('esNodeVersionCompatibility$ only starts polling when subscribed to', async done => { + const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce( + () => mockAdminClusterClientInstance + ).mockImplementationOnce(() => mockDataClusterClientInstance); + + mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + + const setupContract = await elasticsearchService.setup(deps); + await delay(10); + + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + setupContract.esNodesCompatibility$.subscribe(() => { + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + done(); + }); + }); + + it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async done => { + const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce( + () => mockAdminClusterClientInstance + ).mockImplementationOnce(() => mockDataClusterClientInstance); + + mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + + const setupContract = await elasticsearchService.setup(deps); + + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + const sub = setupContract.esNodesCompatibility$.subscribe(async () => { + sub.unsubscribe(); + await delay(100); + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + done(); + }); + }); }); describe('#stop', () => { @@ -229,4 +271,27 @@ describe('#stop', () => { expect(mockAdminClusterClientInstance.close).toHaveBeenCalledTimes(1); expect(mockDataClusterClientInstance.close).toHaveBeenCalledTimes(1); }); + + it('stops pollEsNodeVersions even if there are active subscriptions', async done => { + expect.assertions(2); + const mockAdminClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); + const mockDataClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); + + MockClusterClient.mockImplementationOnce( + () => mockAdminClusterClientInstance + ).mockImplementationOnce(() => mockDataClusterClientInstance); + + mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + + const setupContract = await elasticsearchService.setup(deps); + + setupContract.esNodesCompatibility$.subscribe(async () => { + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + + await elasticsearchService.stop(); + await delay(100); + expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + done(); + }); + }); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 9eaf125cc006fc..6616b42f136c04 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -17,8 +17,17 @@ * under the License. */ -import { ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { filter, first, map, publishReplay, switchMap, take } from 'rxjs/operators'; +import { ConnectableObservable, Observable, Subscription, Subject } from 'rxjs'; +import { + filter, + first, + map, + publishReplay, + switchMap, + take, + shareReplay, + takeUntil, +} from 'rxjs/operators'; import { CoreService } from '../../types'; import { merge } from '../../utils'; @@ -47,13 +56,8 @@ interface SetupDeps { export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private subscriptions: { - client?: Subscription; - esNodesCompatibility?: Subscription; - } = { - client: undefined, - esNodesCompatibility: undefined, - }; + private subscription: Subscription | undefined; + private stop$ = new Subject(); private kibanaVersion: string; constructor(private readonly coreContext: CoreContext) { @@ -69,7 +73,7 @@ export class ElasticsearchService implements CoreService { - if (this.subscriptions.client !== undefined) { + if (this.subscription !== undefined) { this.log.error('Clients cannot be changed after they are created'); return false; } @@ -100,7 +104,7 @@ export class ElasticsearchService implements CoreService; - this.subscriptions.client = clients$.connect(); + this.subscription = clients$.connect(); const config = await this.config$.pipe(first()).toPromise(); @@ -164,18 +168,7 @@ export class ElasticsearchService implements CoreService).connect(); - - // TODO: Move to Status Service https://github.com/elastic/kibana/issues/41983 - esNodesCompatibility$.subscribe(({ isCompatible, message }) => { - if (!isCompatible && message) { - this.log.error(message); - } - }); + }).pipe(takeUntil(this.stop$), shareReplay({ refCount: true, bufferSize: 1 })); return { legacy: { config$: clients$.pipe(map(clients => clients.config)) }, @@ -195,12 +188,10 @@ export class ElasticsearchService implements CoreService { }); }); + it('retries ES API calls that rejects with snapshot_in_progress_exception', () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + return i++ <= 2 + ? Promise.reject({ body: { error: { type: 'snapshot_in_progress_exception' } } }) + : Promise.resolve('success'); + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + it('rejects when ES API calls reject with other errors', async () => { expect.assertions(3); const callEsApi = jest.fn(); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts index ea3cc0b90c0778..901b801159cb6a 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -64,7 +64,8 @@ export function migrationsRetryCallCluster( error instanceof esErrors.AuthenticationException || error instanceof esErrors.AuthorizationException || // @ts-ignore - error instanceof esErrors.Gone + error instanceof esErrors.Gone || + error?.body?.error?.type === 'snapshot_in_progress_exception' ); }, timer(delay), @@ -85,15 +86,7 @@ export function migrationsRetryCallCluster( * * @param apiCaller */ - -// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged -export function retryCallCluster( - apiCaller: ( - endpoint: string, - clientParams: Record, - options?: CallAPIOptions - ) => Promise -) { +export function retryCallCluster(apiCaller: APICaller) { return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { return defer(() => apiCaller(endpoint, clientParams, options)) .pipe( diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 97fab4bc2f6ea2..2de233aa74cd3c 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -29,6 +29,7 @@ import { RouteMethod, KibanaResponseFactory, RouteValidationSpec, + KibanaRouteState, } from './router'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; @@ -43,6 +44,7 @@ interface RequestFixtureOptions

{ method?: RouteMethod; socket?: Socket; routeTags?: string[]; + kibanaRouteState?: KibanaRouteState; routeAuthRequired?: false; validation?: { params?: RouteValidationSpec

; @@ -62,6 +64,7 @@ function createKibanaRequestMock

({ routeTags, routeAuthRequired, validation = {}, + kibanaRouteState = { xsrfRequired: true }, }: RequestFixtureOptions = {}) { const queryString = querystring.stringify(query); @@ -80,7 +83,7 @@ function createKibanaRequestMock

({ search: queryString ? `?${queryString}` : queryString, }, route: { - settings: { tags: routeTags, auth: routeAuthRequired }, + settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteState }, }, raw: { req: { socket }, @@ -109,6 +112,7 @@ function createRawRequestMock(customization: DeepPartial = {}) { return merge( {}, { + app: { xsrfRequired: true } as any, headers: {}, path: '/', route: { settings: {} }, diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index a9fc80c86d878e..27db79bb94d252 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -811,6 +811,7 @@ test('exposes route details of incoming request to a route handler', async () => path: '/', options: { authRequired: true, + xsrfRequired: false, tags: [], }, }); @@ -923,6 +924,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo path: '/', options: { authRequired: true, + xsrfRequired: true, tags: [], body: { parse: true, // hapi populates the default diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 025ab2bf56ac2d..cffdffab0d0cf7 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -27,7 +27,7 @@ import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_p import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; -import { IRouter } from './router'; +import { IRouter, KibanaRouteState, isSafeMethod } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -147,9 +147,14 @@ export class HttpServer { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); // Hapi does not allow payload validation to be specified for 'head' or 'get' requests - const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; + const validate = isSafeMethod(route.method) ? undefined : { payload: true }; const { authRequired = true, tags, body = {} } = route.options; const { accepts: allow, maxBytes, output, parse } = body; + + const kibanaRouteState: KibanaRouteState = { + xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method), + }; + this.server.route({ handler: route.handler, method: route.method, @@ -157,6 +162,7 @@ export class HttpServer { options: { // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }` auth: authRequired === true ? undefined : false, + app: kibanaRouteState, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index d31afe1670e419..8f4c02680f8a30 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -58,6 +58,8 @@ export { RouteValidationError, RouteValidatorFullConfig, RouteValidationResultFactory, + DestructiveRouteMethod, + SafeRouteMethod, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index f4c5f16870c7ed..b5364c616f17cf 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -36,6 +36,7 @@ const versionHeader = 'kbn-version'; const xsrfHeader = 'kbn-xsrf'; const nameHeader = 'kbn-name'; const whitelistedTestPath = '/xsrf/test/route/whitelisted'; +const xsrfDisabledTestPath = '/xsrf/test/route/disabled'; const kibanaName = 'my-kibana-name'; const setupDeps = { context: contextServiceMock.createSetupContract(), @@ -188,6 +189,12 @@ describe('core lifecycle handlers', () => { return res.ok({ body: 'ok' }); } ); + ((router as any)[method.toLowerCase()] as RouteRegistrar)( + { path: xsrfDisabledTestPath, validate: false, options: { xsrfRequired: false } }, + (context, req, res) => { + return res.ok({ body: 'ok' }); + } + ); }); await server.start(); @@ -235,6 +242,10 @@ describe('core lifecycle handlers', () => { it('accepts whitelisted requests without either an xsrf or version header', async () => { await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok'); }); + + it('accepts requests on a route with disabled xsrf protection', async () => { + await getSupertest(method.toLowerCase(), xsrfDisabledTestPath).expect(200, 'ok'); + }); }); }); }); diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index 48a6973b741ba0..a80e432e0d4cb7 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -24,7 +24,7 @@ import { } from './lifecycle_handlers'; import { httpServerMock } from './http_server.mocks'; import { HttpConfig } from './http_config'; -import { KibanaRequest, RouteMethod } from './router'; +import { KibanaRequest, RouteMethod, KibanaRouteState } from './router'; const createConfig = (partial: Partial): HttpConfig => partial as HttpConfig; @@ -32,12 +32,14 @@ const forgeRequest = ({ headers = {}, path = '/', method = 'get', + kibanaRouteState, }: Partial<{ headers: Record; path: string; method: RouteMethod; + kibanaRouteState: KibanaRouteState; }>): KibanaRequest => { - return httpServerMock.createKibanaRequest({ headers, path, method }); + return httpServerMock.createKibanaRequest({ headers, path, method, kibanaRouteState }); }; describe('xsrf post-auth handler', () => { @@ -142,6 +144,29 @@ describe('xsrf post-auth handler', () => { expect(toolkit.next).toHaveBeenCalledTimes(1); expect(result).toEqual('next'); }); + + it('accepts requests if xsrf protection on a route is disabled', () => { + const config = createConfig({ + xsrf: { whitelist: [], disableProtection: false }, + }); + const handler = createXsrfPostAuthHandler(config); + const request = forgeRequest({ + method: 'post', + headers: {}, + path: '/some-path', + kibanaRouteState: { + xsrfRequired: false, + }, + }); + + toolkit.next.mockReturnValue('next' as any); + + const result = handler(request, responseFactory, toolkit); + + expect(responseFactory.badRequest).not.toHaveBeenCalled(); + expect(toolkit.next).toHaveBeenCalledTimes(1); + expect(result).toEqual('next'); + }); }); }); diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts index ee877ee031a2bb..7ef7e863260391 100644 --- a/src/core/server/http/lifecycle_handlers.ts +++ b/src/core/server/http/lifecycle_handlers.ts @@ -20,6 +20,7 @@ import { OnPostAuthHandler } from './lifecycle/on_post_auth'; import { OnPreResponseHandler } from './lifecycle/on_pre_response'; import { HttpConfig } from './http_config'; +import { isSafeMethod } from './router'; import { Env } from '../config'; import { LifecycleRegistrar } from './http_server'; @@ -31,15 +32,18 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler const { whitelist, disableProtection } = config.xsrf; return (request, response, toolkit) => { - if (disableProtection || whitelist.includes(request.route.path)) { + if ( + disableProtection || + whitelist.includes(request.route.path) || + request.route.options.xsrfRequired === false + ) { return toolkit.next(); } - const isSafeMethod = request.route.method === 'get' || request.route.method === 'head'; const hasVersionHeader = VERSION_HEADER in request.headers; const hasXsrfHeader = XSRF_HEADER in request.headers; - if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) { + if (!isSafeMethod(request.route.method) && !hasVersionHeader && !hasXsrfHeader) { return response.badRequest({ body: `Request must contain a ${XSRF_HEADER} header.` }); } diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 32663d1513f36b..d254f391ca5e41 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -24,16 +24,20 @@ export { KibanaRequestEvents, KibanaRequestRoute, KibanaRequestRouteOptions, + KibanaRouteState, isRealRequest, LegacyRequest, ensureRawRequest, } from './request'; export { + DestructiveRouteMethod, + isSafeMethod, RouteMethod, RouteConfig, RouteConfigOptions, RouteContentType, RouteConfigOptionsBody, + SafeRouteMethod, validBodyOutput, } from './route'; export { HapiResponseAdapter } from './response_adapter'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 703571ba53c0a3..bb2db6367f701f 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -18,18 +18,24 @@ */ import { Url } from 'url'; -import { Request } from 'hapi'; +import { Request, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; import { shareReplay, first, takeUntil } from 'rxjs/operators'; import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { Headers } from './headers'; -import { RouteMethod, RouteConfigOptions, validBodyOutput } from './route'; +import { RouteMethod, RouteConfigOptions, validBodyOutput, isSafeMethod } from './route'; import { KibanaSocket, IKibanaSocket } from './socket'; import { RouteValidator, RouteValidatorFullConfig } from './validator'; const requestSymbol = Symbol('request'); +/** + * @internal + */ +export interface KibanaRouteState extends ApplicationState { + xsrfRequired: boolean; +} /** * Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. * @public @@ -184,8 +190,10 @@ export class KibanaRequest< const options = ({ authRequired: request.route.settings.auth !== false, + // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8 + xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true, tags: request.route.settings.tags || [], - body: ['get', 'options'].includes(method) + body: isSafeMethod(method) ? undefined : { parse, diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 4439a80b1eac71..d1458ef4ad0632 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -19,11 +19,27 @@ import { RouteValidatorFullConfig } from './validator'; +export function isSafeMethod(method: RouteMethod): method is SafeRouteMethod { + return method === 'get' || method === 'options'; +} + +/** + * Set of HTTP methods changing the state of the server. + * @public + */ +export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; + +/** + * Set of HTTP methods not changing the state of the server. + * @public + */ +export type SafeRouteMethod = 'get' | 'options'; + /** * The set of common HTTP methods supported by Kibana routing. * @public */ -export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; +export type RouteMethod = SafeRouteMethod | DestructiveRouteMethod; /** * The set of valid body.output @@ -108,6 +124,15 @@ export interface RouteConfigOptions { */ authRequired?: boolean; + /** + * Defines xsrf protection requirements for a route: + * - true. Requires an incoming POST/PUT/DELETE request to contain `kbn-xsrf` header. + * - false. Disables xsrf protection. + * + * Set to true by default + */ + xsrfRequired?: Method extends 'get' ? never : boolean; + /** * Additional metadata tag strings to attach to the route. */ diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e45d4f28edcc37..8e481171116faf 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -54,6 +54,7 @@ import { SavedObjectsClientContract } from './saved_objects/types'; import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; +import { MetricsServiceSetup } from './metrics'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; @@ -159,6 +160,8 @@ export { SessionStorageCookieOptions, SessionCookieValidationResult, SessionStorageFactory, + DestructiveRouteMethod, + SafeRouteMethod, } from './http'; export { RenderingServiceSetup, IRenderOptions } from './rendering'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; @@ -229,6 +232,9 @@ export { SavedObjectsType, SavedObjectMigrationMap, SavedObjectMigrationFn, + exportSavedObjectsToStream, + importSavedObjectsFromStream, + resolveSavedObjectsImportErrors, } from './saved_objects'; export { @@ -245,6 +251,14 @@ export { StringValidationRegexString, } from './ui_settings'; +export { + OpsMetrics, + OpsOsMetrics, + OpsServerMetrics, + OpsProcessMetrics, + MetricsServiceSetup, +} from './metrics'; + export { RecursiveReadonly } from '../utils'; export { @@ -322,6 +336,8 @@ export interface CoreSetup { uiSettings: UiSettingsServiceSetup; /** {@link UuidServiceSetup} */ uuid: UuidServiceSetup; + /** {@link MetricsServiceSetup} */ + metrics: MetricsServiceSetup; /** * Allows plugins to get access to APIs available in start inside async handlers. * Promise will not resolve until Core and plugin dependencies have completed `start`. diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index ff68d1544d119e..37d1061dc618dc 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -30,6 +30,7 @@ import { } from './saved_objects'; import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { UuidServiceSetup } from './uuid'; +import { InternalMetricsServiceSetup } from './metrics'; /** @internal */ export interface InternalCoreSetup { @@ -40,6 +41,7 @@ export interface InternalCoreSetup { uiSettings: InternalUiSettingsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; uuid: UuidServiceSetup; + metrics: InternalMetricsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 46436461505c06..50468db8a504de 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -43,6 +43,7 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service. import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { uuidServiceMock } from '../uuid/uuid_service.mock'; +import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { findLegacyPluginSpecs } from './plugins'; import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; @@ -93,6 +94,7 @@ beforeEach(() => { }, }, rendering: renderingServiceMock, + metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 44f77b5ad215e7..f67148d720446a 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -296,10 +296,14 @@ export class LegacyService implements CoreService { isTlsEnabled: setupDeps.core.http.isTlsEnabled, getServerInfo: setupDeps.core.http.getServerInfo, }, + metrics: { + getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, registerType: setupDeps.core.savedObjects.registerType, + getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/core/server/metrics/collectors/index.ts similarity index 76% rename from src/legacy/core_plugins/visualizations/public/legacy_imports.ts rename to src/core/server/metrics/collectors/index.ts index 0a3b1938436c0e..f58ab02e638813 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/core/server/metrics/collectors/index.ts @@ -17,11 +17,7 @@ * under the License. */ -export { - IAggConfig, - IAggConfigs, - isDateHistogramBucketAggConfig, - setBounds, -} from '../../data/public'; -export { createAggConfigs } from 'ui/agg_types'; -export { createSavedSearchesLoader } from '../../../../plugins/discover/public'; +export { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics, MetricsCollector } from './types'; +export { OsMetricsCollector } from './os'; +export { ProcessMetricsCollector } from './process'; +export { ServerMetricsCollector } from './server'; diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts new file mode 100644 index 00000000000000..7d5a6da90b7d62 --- /dev/null +++ b/src/core/server/metrics/collectors/os.test.ts @@ -0,0 +1,99 @@ +/* + * 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. + */ + +jest.mock('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' })); + +import os from 'os'; +import { OsMetricsCollector } from './os'; + +describe('OsMetricsCollector', () => { + let collector: OsMetricsCollector; + + beforeEach(() => { + collector = new OsMetricsCollector(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('collects platform info from the os package', async () => { + const platform = 'darwin'; + const release = '10.14.1'; + + jest.spyOn(os, 'platform').mockImplementation(() => platform); + jest.spyOn(os, 'release').mockImplementation(() => release); + + const metrics = await collector.collect(); + + expect(metrics.platform).toBe(platform); + expect(metrics.platformRelease).toBe(`${platform}-${release}`); + }); + + it('collects distribution info when platform is linux', async () => { + const platform = 'linux'; + + jest.spyOn(os, 'platform').mockImplementation(() => platform); + + const metrics = await collector.collect(); + + expect(metrics.distro).toBe('distrib'); + expect(metrics.distroRelease).toBe('distrib-release'); + }); + + it('collects memory info from the os package', async () => { + const totalMemory = 1457886; + const freeMemory = 456786; + + jest.spyOn(os, 'totalmem').mockImplementation(() => totalMemory); + jest.spyOn(os, 'freemem').mockImplementation(() => freeMemory); + + const metrics = await collector.collect(); + + expect(metrics.memory.total_in_bytes).toBe(totalMemory); + expect(metrics.memory.free_in_bytes).toBe(freeMemory); + expect(metrics.memory.used_in_bytes).toBe(totalMemory - freeMemory); + }); + + it('collects uptime info from the os package', async () => { + const uptime = 325; + + jest.spyOn(os, 'uptime').mockImplementation(() => uptime); + + const metrics = await collector.collect(); + + expect(metrics.uptime_in_millis).toBe(uptime * 1000); + }); + + it('collects load info from the os package', async () => { + const oneMinLoad = 1; + const fiveMinLoad = 2; + const fifteenMinLoad = 3; + + jest.spyOn(os, 'loadavg').mockImplementation(() => [oneMinLoad, fiveMinLoad, fifteenMinLoad]); + + const metrics = await collector.collect(); + + expect(metrics.load).toEqual({ + '1m': oneMinLoad, + '5m': fiveMinLoad, + '15m': fifteenMinLoad, + }); + }); +}); diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts new file mode 100644 index 00000000000000..d3d9bb0be86fa2 --- /dev/null +++ b/src/core/server/metrics/collectors/os.ts @@ -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 os from 'os'; +import getosAsync, { LinuxOs } from 'getos'; +import { promisify } from 'util'; +import { OpsOsMetrics, MetricsCollector } from './types'; + +const getos = promisify(getosAsync); + +export class OsMetricsCollector implements MetricsCollector { + public async collect(): Promise { + const platform = os.platform(); + const load = os.loadavg(); + + const metrics: OpsOsMetrics = { + platform, + platformRelease: `${platform}-${os.release()}`, + load: { + '1m': load[0], + '5m': load[1], + '15m': load[2], + }, + memory: { + total_in_bytes: os.totalmem(), + free_in_bytes: os.freemem(), + used_in_bytes: os.totalmem() - os.freemem(), + }, + uptime_in_millis: os.uptime() * 1000, + }; + + if (platform === 'linux') { + try { + const distro = (await getos()) as LinuxOs; + metrics.distro = distro.dist; + metrics.distroRelease = `${distro.dist}-${distro.release}`; + } catch (e) { + // ignore errors + } + } + + return metrics; + } +} diff --git a/src/core/server/metrics/collectors/process.test.ts b/src/core/server/metrics/collectors/process.test.ts new file mode 100644 index 00000000000000..a437d799371f11 --- /dev/null +++ b/src/core/server/metrics/collectors/process.test.ts @@ -0,0 +1,81 @@ +/* + * 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 v8, { HeapInfo } from 'v8'; +import { ProcessMetricsCollector } from './process'; + +describe('ProcessMetricsCollector', () => { + let collector: ProcessMetricsCollector; + + beforeEach(() => { + collector = new ProcessMetricsCollector(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('collects pid from the process', async () => { + const metrics = await collector.collect(); + + expect(metrics.pid).toEqual(process.pid); + }); + + it('collects event loop delay', async () => { + const metrics = await collector.collect(); + + expect(metrics.event_loop_delay).toBeGreaterThan(0); + }); + + it('collects uptime info from the process', async () => { + const uptime = 58986; + jest.spyOn(process, 'uptime').mockImplementation(() => uptime); + + const metrics = await collector.collect(); + + expect(metrics.uptime_in_millis).toEqual(uptime * 1000); + }); + + it('collects memory info from the process', async () => { + const heapTotal = 58986; + const heapUsed = 4688; + const heapSizeLimit = 5788; + const rss = 5865; + jest.spyOn(process, 'memoryUsage').mockImplementation(() => ({ + rss, + heapTotal, + heapUsed, + external: 0, + })); + + jest.spyOn(v8, 'getHeapStatistics').mockImplementation( + () => + ({ + heap_size_limit: heapSizeLimit, + } as HeapInfo) + ); + + const metrics = await collector.collect(); + + expect(metrics.memory.heap.total_in_bytes).toEqual(heapTotal); + expect(metrics.memory.heap.used_in_bytes).toEqual(heapUsed); + expect(metrics.memory.heap.size_limit).toEqual(heapSizeLimit); + expect(metrics.memory.resident_set_size_in_bytes).toEqual(rss); + }); +}); diff --git a/src/core/server/metrics/collectors/process.ts b/src/core/server/metrics/collectors/process.ts new file mode 100644 index 00000000000000..aa68abaf74e41a --- /dev/null +++ b/src/core/server/metrics/collectors/process.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 v8 from 'v8'; +import { Bench } from 'hoek'; +import { OpsProcessMetrics, MetricsCollector } from './types'; + +export class ProcessMetricsCollector implements MetricsCollector { + public async collect(): Promise { + const heapStats = v8.getHeapStatistics(); + const memoryUsage = process.memoryUsage(); + const [eventLoopDelay] = await Promise.all([getEventLoopDelay()]); + return { + memory: { + heap: { + total_in_bytes: memoryUsage.heapTotal, + used_in_bytes: memoryUsage.heapUsed, + size_limit: heapStats.heap_size_limit, + }, + resident_set_size_in_bytes: memoryUsage.rss, + }, + pid: process.pid, + event_loop_delay: eventLoopDelay, + uptime_in_millis: process.uptime() * 1000, + }; + } +} + +const getEventLoopDelay = (): Promise => { + const bench = new Bench(); + return new Promise(resolve => { + setImmediate(() => { + return resolve(bench.elapsed()); + }); + }); +}; diff --git a/src/core/server/metrics/collectors/server.ts b/src/core/server/metrics/collectors/server.ts new file mode 100644 index 00000000000000..e46ac2f653df66 --- /dev/null +++ b/src/core/server/metrics/collectors/server.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 { ResponseObject, Server as HapiServer } from 'hapi'; +import { OpsServerMetrics, MetricsCollector } from './types'; + +interface ServerResponseTime { + count: number; + total: number; + max: number; +} + +export class ServerMetricsCollector implements MetricsCollector { + private readonly requests: OpsServerMetrics['requests'] = { + disconnects: 0, + total: 0, + statusCodes: {}, + }; + private readonly responseTimes: ServerResponseTime = { + count: 0, + total: 0, + max: 0, + }; + + constructor(private readonly server: HapiServer) { + this.server.ext('onRequest', (request, h) => { + this.requests.total++; + request.events.once('disconnect', () => { + this.requests.disconnects++; + }); + return h.continue; + }); + this.server.events.on('response', request => { + const statusCode = (request.response as ResponseObject)?.statusCode; + if (statusCode) { + if (!this.requests.statusCodes[statusCode]) { + this.requests.statusCodes[statusCode] = 0; + } + this.requests.statusCodes[statusCode]++; + } + + const duration = Date.now() - request.info.received; + this.responseTimes.count++; + this.responseTimes.total += duration; + this.responseTimes.max = Math.max(this.responseTimes.max, duration); + }); + } + + public async collect(): Promise { + const connections = await new Promise(resolve => { + this.server.listener.getConnections((_, count) => { + resolve(count); + }); + }); + + return { + requests: this.requests, + response_times: { + avg_in_millis: this.responseTimes.total / Math.max(this.responseTimes.count, 1), + max_in_millis: this.responseTimes.max, + }, + concurrent_connections: connections, + }; + } +} diff --git a/src/core/server/metrics/collectors/types.ts b/src/core/server/metrics/collectors/types.ts new file mode 100644 index 00000000000000..5a83bc70af3c11 --- /dev/null +++ b/src/core/server/metrics/collectors/types.ts @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/** Base interface for all metrics gatherers */ +export interface MetricsCollector { + collect(): Promise; +} + +/** + * Process related metrics + * @public + */ +export interface OpsProcessMetrics { + /** process memory usage */ + memory: { + /** heap memory usage */ + heap: { + /** total heap available */ + total_in_bytes: number; + /** used heap */ + used_in_bytes: number; + /** v8 heap size limit */ + size_limit: number; + }; + /** node rss */ + resident_set_size_in_bytes: number; + }; + /** node event loop delay */ + event_loop_delay: number; + /** pid of the kibana process */ + pid: number; + /** uptime of the kibana process */ + uptime_in_millis: number; +} + +/** + * OS related metrics + * @public + */ +export interface OpsOsMetrics { + /** The os platform */ + platform: NodeJS.Platform; + /** The os platform release, prefixed by the platform name */ + platformRelease: string; + /** The os distrib. Only present for linux platforms */ + distro?: string; + /** The os distrib release, prefixed by the os distrib. Only present for linux platforms */ + distroRelease?: string; + /** cpu load metrics */ + load: { + /** load for last minute */ + '1m': number; + /** load for last 5 minutes */ + '5m': number; + /** load for last 15 minutes */ + '15m': number; + }; + /** system memory usage metrics */ + memory: { + /** total memory available */ + total_in_bytes: number; + /** current free memory */ + free_in_bytes: number; + /** current used memory */ + used_in_bytes: number; + }; + /** the OS uptime */ + uptime_in_millis: number; +} + +/** + * server related metrics + * @public + */ +export interface OpsServerMetrics { + /** server response time stats */ + response_times: { + /** average response time */ + avg_in_millis: number; + /** maximum response time */ + max_in_millis: number; + }; + /** server requests stats */ + requests: { + /** number of disconnected requests since startup */ + disconnects: number; + /** total number of requests handled since startup */ + total: number; + /** number of request handled per response status code */ + statusCodes: Record; + }; + /** number of current concurrent connections to the server */ + concurrent_connections: number; +} diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts new file mode 100644 index 00000000000000..fdcf637c0cd7b9 --- /dev/null +++ b/src/core/server/metrics/index.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. + */ + +export { + InternalMetricsServiceStart, + InternalMetricsServiceSetup, + MetricsServiceSetup, + MetricsServiceStart, + OpsMetrics, +} from './types'; +export { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; +export { MetricsService } from './metrics_service'; +export { opsConfig } from './ops_config'; diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts new file mode 100644 index 00000000000000..6baf95894b9b42 --- /dev/null +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -0,0 +1,203 @@ +/* + * 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 { BehaviorSubject, Subject } from 'rxjs'; +import { take, filter } from 'rxjs/operators'; +import supertest from 'supertest'; +import { Server as HapiServer } from 'hapi'; +import { createHttpServer } from '../../http/test_utils'; +import { HttpService, IRouter } from '../../http'; +import { contextServiceMock } from '../../context/context_service.mock'; +import { ServerMetricsCollector } from '../collectors/server'; + +const requestWaitDelay = 25; + +describe('ServerMetricsCollector', () => { + let server: HttpService; + let collector: ServerMetricsCollector; + let hapiServer: HapiServer; + let router: IRouter; + + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const sendGet = (path: string) => supertest(hapiServer.listener).get(path); + + beforeEach(async () => { + server = createHttpServer(); + const contextSetup = contextServiceMock.createSetupContract(); + const httpSetup = await server.setup({ context: contextSetup }); + hapiServer = httpSetup.server; + router = httpSetup.createRouter('/'); + collector = new ServerMetricsCollector(hapiServer); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('collect requests infos', async () => { + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + await server.start(); + + let metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 0, + disconnects: 0, + statusCodes: {}, + }); + + await sendGet('/'); + await sendGet('/'); + await sendGet('/not-found'); + + metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 3, + disconnects: 0, + statusCodes: { + '200': 2, + '404': 1, + }, + }); + }); + + it('collect disconnects requests infos', async () => { + const never = new Promise(resolve => undefined); + const hitSubject = new BehaviorSubject(0); + + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/disconnect', validate: false }, async (ctx, req, res) => { + hitSubject.next(hitSubject.value + 1); + await never; + return res.ok({ body: '' }); + }); + await server.start(); + + await sendGet('/'); + const discoReq1 = sendGet('/disconnect').end(); + const discoReq2 = sendGet('/disconnect').end(); + + await hitSubject + .pipe( + filter(count => count >= 2), + take(1) + ) + .toPromise(); + + let metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 0, + }) + ); + + discoReq1.abort(); + await delay(requestWaitDelay); + + metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 1, + }) + ); + + discoReq2.abort(); + await delay(requestWaitDelay); + + metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 2, + }) + ); + }); + + it('collect response times', async () => { + router.get({ path: '/no-delay', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/500-ms', validate: false }, async (ctx, req, res) => { + await delay(500); + return res.ok({ body: '' }); + }); + router.get({ path: '/250-ms', validate: false }, async (ctx, req, res) => { + await delay(250); + return res.ok({ body: '' }); + }); + await server.start(); + + await Promise.all([sendGet('/no-delay'), sendGet('/250-ms')]); + let metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(125); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(250); + + await Promise.all([sendGet('/500-ms'), sendGet('/500-ms')]); + metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(250); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500); + }); + + it('collect connection count', async () => { + const waitSubject = new Subject(); + const hitSubject = new BehaviorSubject(0); + + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + hitSubject.next(hitSubject.value + 1); + await waitSubject.pipe(take(1)).toPromise(); + return res.ok({ body: '' }); + }); + await server.start(); + + const waitForHits = (hits: number) => + hitSubject + .pipe( + filter(count => count >= hits), + take(1) + ) + .toPromise(); + + let metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(0); + + sendGet('/').end(() => null); + await waitForHits(1); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(1); + + sendGet('/').end(() => null); + await waitForHits(2); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(2); + + waitSubject.next('go'); + await delay(requestWaitDelay); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(0); + }); +}); diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts new file mode 100644 index 00000000000000..cc53a4e27d5710 --- /dev/null +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -0,0 +1,67 @@ +/* + * 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 { MetricsService } from './metrics_service'; +import { + InternalMetricsServiceSetup, + InternalMetricsServiceStart, + MetricsServiceSetup, + MetricsServiceStart, +} from './types'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + getOpsMetrics$: jest.fn(), + }; + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const setupContract: jest.Mocked = createSetupContractMock(); + return setupContract; +}; + +const createStartContractMock = () => { + const startContract: jest.Mocked = {}; + return startContract; +}; + +const createInternalStartContractMock = () => { + const startContract: jest.Mocked = createStartContractMock(); + return startContract; +}; + +type MetricsServiceContract = PublicMethodsOf; + +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn().mockReturnValue(createInternalStartContractMock()), + stop: jest.fn(), + }; + return mocked; +}; + +export const metricsServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, + createInternalSetupContract: createInternalSetupContractMock, + createInternalStartContract: createInternalStartContractMock, +}; diff --git a/src/core/server/metrics/metrics_service.test.mocks.ts b/src/core/server/metrics/metrics_service.test.mocks.ts new file mode 100644 index 00000000000000..8e91775283042b --- /dev/null +++ b/src/core/server/metrics/metrics_service.test.mocks.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 const mockOpsCollector = { + collect: jest.fn(), +}; +jest.doMock('./ops_metrics_collector', () => ({ + OpsMetricsCollector: jest.fn().mockImplementation(() => mockOpsCollector), +})); diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts new file mode 100644 index 00000000000000..10d6761adbe7d5 --- /dev/null +++ b/src/core/server/metrics/metrics_service.test.ts @@ -0,0 +1,134 @@ +/* + * 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 moment from 'moment'; +import { mockOpsCollector } from './metrics_service.test.mocks'; +import { MetricsService } from './metrics_service'; +import { mockCoreContext } from '../core_context.mock'; +import { configServiceMock } from '../config/config_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { take } from 'rxjs/operators'; + +const testInterval = 100; + +const dummyMetrics = { metricA: 'value', metricB: 'otherValue' }; + +describe('MetricsService', () => { + const httpMock = httpServiceMock.createSetupContract(); + let metricsService: MetricsService; + + beforeEach(() => { + jest.useFakeTimers(); + + const configService = configServiceMock.create({ + atPath: { interval: moment.duration(testInterval) }, + }); + const coreContext = mockCoreContext.create({ configService }); + metricsService = new MetricsService(coreContext); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + describe('#start', () => { + it('invokes setInterval with the configured interval', async () => { + await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval); + }); + + it('emits the metrics at start', async () => { + mockOpsCollector.collect.mockResolvedValue(dummyMetrics); + + const { getOpsMetrics$ } = await metricsService.setup({ + http: httpMock, + }); + + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + expect( + await getOpsMetrics$() + .pipe(take(1)) + .toPromise() + ).toEqual(dummyMetrics); + }); + + it('collects the metrics at every interval', async () => { + mockOpsCollector.collect.mockResolvedValue(dummyMetrics); + + await metricsService.setup({ http: httpMock }); + + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3); + }); + + it('throws when called before setup', async () => { + await expect(metricsService.start()).rejects.toThrowErrorMatchingInlineSnapshot( + `"#setup() needs to be run first"` + ); + }); + }); + + describe('#stop', () => { + it('stops the metrics interval', async () => { + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + await metricsService.stop(); + jest.advanceTimersByTime(10 * testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + getOpsMetrics$().subscribe({ complete: () => {} }); + }); + + it('completes the metrics observable', async () => { + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + let completed = false; + + getOpsMetrics$().subscribe({ + complete: () => { + completed = true; + }, + }); + + await metricsService.stop(); + + expect(completed).toEqual(true); + }); + }); +}); diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts new file mode 100644 index 00000000000000..1aed89a4aad600 --- /dev/null +++ b/src/core/server/metrics/metrics_service.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ReplaySubject } from 'rxjs'; +import { first, shareReplay } from 'rxjs/operators'; +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { InternalHttpServiceSetup } from '../http'; +import { InternalMetricsServiceSetup, InternalMetricsServiceStart, OpsMetrics } from './types'; +import { OpsMetricsCollector } from './ops_metrics_collector'; +import { opsConfig, OpsConfigType } from './ops_config'; + +interface MetricsServiceSetupDeps { + http: InternalHttpServiceSetup; +} + +/** @internal */ +export class MetricsService + implements CoreService { + private readonly logger: Logger; + private metricsCollector?: OpsMetricsCollector; + private collectInterval?: NodeJS.Timeout; + private metrics$ = new ReplaySubject(1); + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('metrics'); + } + + public async setup({ http }: MetricsServiceSetupDeps): Promise { + this.metricsCollector = new OpsMetricsCollector(http.server); + + const metricsObservable = this.metrics$.pipe(shareReplay(1)); + + return { + getOpsMetrics$: () => metricsObservable, + }; + } + + public async start(): Promise { + if (!this.metricsCollector) { + throw new Error('#setup() needs to be run first'); + } + const config = await this.coreContext.configService + .atPath(opsConfig.path) + .pipe(first()) + .toPromise(); + + await this.refreshMetrics(); + + this.collectInterval = setInterval(() => { + this.refreshMetrics(); + }, config.interval.asMilliseconds()); + + return {}; + } + + private async refreshMetrics() { + this.logger.debug('Refreshing metrics'); + const metrics = await this.metricsCollector!.collect(); + this.metrics$.next(metrics); + } + + public async stop() { + if (this.collectInterval) { + clearInterval(this.collectInterval); + } + this.metrics$.complete(); + } +} diff --git a/src/core/server/metrics/ops_config.ts b/src/core/server/metrics/ops_config.ts new file mode 100644 index 00000000000000..bd6ae5cc5474d7 --- /dev/null +++ b/src/core/server/metrics/ops_config.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 { schema, TypeOf } from '@kbn/config-schema'; + +export const opsConfig = { + path: 'ops', + schema: schema.object({ + interval: schema.duration({ defaultValue: '5s' }), + }), +}; + +export type OpsConfigType = TypeOf; diff --git a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts new file mode 100644 index 00000000000000..8265796d57970e --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts @@ -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. + */ + +export const mockOsCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/os', () => ({ + OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector), +})); + +export const mockProcessCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/process', () => ({ + ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector), +})); + +export const mockServerCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/server', () => ({ + ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector), +})); diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts new file mode 100644 index 00000000000000..04302a195fb6cd --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + mockOsCollector, + mockProcessCollector, + mockServerCollector, +} from './ops_metrics_collector.test.mocks'; +import { httpServiceMock } from '../http/http_service.mock'; +import { OpsMetricsCollector } from './ops_metrics_collector'; + +describe('OpsMetricsCollector', () => { + let collector: OpsMetricsCollector; + + beforeEach(() => { + const hapiServer = httpServiceMock.createSetupContract().server; + collector = new OpsMetricsCollector(hapiServer); + + mockOsCollector.collect.mockResolvedValue('osMetrics'); + }); + + it('gathers metrics from the underlying collectors', async () => { + mockOsCollector.collect.mockResolvedValue('osMetrics'); + mockProcessCollector.collect.mockResolvedValue('processMetrics'); + mockServerCollector.collect.mockResolvedValue({ + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + + const metrics = await collector.collect(); + + expect(mockOsCollector.collect).toHaveBeenCalledTimes(1); + expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1); + expect(mockServerCollector.collect).toHaveBeenCalledTimes(1); + + expect(metrics).toEqual({ + process: 'processMetrics', + os: 'osMetrics', + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + }); +}); diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts new file mode 100644 index 00000000000000..04344f21f57f72 --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.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 { Server as HapiServer } from 'hapi'; +import { + ProcessMetricsCollector, + OsMetricsCollector, + ServerMetricsCollector, + MetricsCollector, +} from './collectors'; +import { OpsMetrics } from './types'; + +export class OpsMetricsCollector implements MetricsCollector { + private readonly processCollector: ProcessMetricsCollector; + private readonly osCollector: OsMetricsCollector; + private readonly serverCollector: ServerMetricsCollector; + + constructor(server: HapiServer) { + this.processCollector = new ProcessMetricsCollector(); + this.osCollector = new OsMetricsCollector(); + this.serverCollector = new ServerMetricsCollector(server); + } + + public async collect(): Promise { + const [process, os, server] = await Promise.all([ + this.processCollector.collect(), + this.osCollector.collect(), + this.serverCollector.collect(), + ]); + return { + process, + os, + ...server, + }; + } +} diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts new file mode 100644 index 00000000000000..5c8f18fff380de --- /dev/null +++ b/src/core/server/metrics/types.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 { Observable } from 'rxjs'; +import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; + +/** + * APIs to retrieves metrics gathered and exposed by the core platform. + * + * @public + */ +export interface MetricsServiceSetup { + /** + * Retrieve an observable emitting the {@link OpsMetrics} gathered. + * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, + * based on the `opts.interval` configuration property. + * + * @example + * ```ts + * core.metrics.getOpsMetrics$().subscribe(metrics => { + * // do something with the metrics + * }) + * ``` + */ + getOpsMetrics$: () => Observable; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MetricsServiceStart {} + +export type InternalMetricsServiceSetup = MetricsServiceSetup; +export type InternalMetricsServiceStart = MetricsServiceStart; + +/** + * Regroups metrics gathered by all the collectors. + * This contains metrics about the os/runtime, the kibana process and the http server. + * + * @public + */ +export interface OpsMetrics { + /** Process related metrics */ + process: OpsProcessMetrics; + /** OS related metrics */ + os: OpsOsMetrics; + /** server response time stats */ + response_times: OpsServerMetrics['response_times']; + /** server requests stats */ + requests: OpsServerMetrics['requests']; + /** number of current concurrent connections to the server */ + concurrent_connections: OpsServerMetrics['concurrent_connections']; +} diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 96b28ab5827e1b..93d8e2c632e385 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -30,6 +30,8 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; +import { metricsServiceMock } from './metrics/metrics_service.mock'; +import { uuidServiceMock } from './uuid/uuid_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -40,7 +42,7 @@ export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; -import { uuidServiceMock } from './uuid/uuid_service.mock'; +export { metricsServiceMock } from './metrics/metrics_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -126,6 +128,7 @@ function createCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), + metrics: metricsServiceMock.createSetupContract(), getStartServices: jest .fn, object]>, []>() .mockResolvedValue([createCoreStartMock(), {}]), @@ -153,6 +156,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index d6c774f6fc41c7..b372874264eb57 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -95,7 +95,7 @@ export class PluginWrapper< public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = this.createPluginInstance(); - this.log.info('Setting up plugin'); + this.log.debug('Setting up plugin'); return this.instance.setup(setupContext, plugins); } @@ -112,6 +112,8 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } + this.log.debug('Starting plugin'); + const startContract = await this.instance.start(startContext, plugins); this.startDependencies$.next([startContext, plugins]); return startContract; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index a8a16713f69a41..b430fd28fb896c 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -166,10 +166,14 @@ export function createPluginSetupContext( isTlsEnabled: deps.http.isTlsEnabled, getServerInfo: deps.http.getServerInfo, }, + metrics: { + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, registerType: deps.savedObjects.registerType, + getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 1088478add1373..32485f461f59b9 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getSortedObjectsForExport } from './get_sorted_objects_for_export'; +import { exportSavedObjectsToStream } from './get_sorted_objects_for_export'; import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '../../../../legacy/utils/streams'; @@ -65,7 +65,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -151,7 +151,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -210,7 +210,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -297,7 +297,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -385,7 +385,7 @@ describe('getSortedObjectsForExport()', () => { page: 0, }); await expect( - getSortedObjectsForExport({ + exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 1, types: ['index-pattern', 'search'], @@ -425,7 +425,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ exportSizeLimit: 10000, savedObjectsClient, types: ['index-pattern'], @@ -489,7 +489,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ exportSizeLimit: 10000, savedObjectsClient, objects: [ @@ -587,7 +587,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ exportSizeLimit: 10000, savedObjectsClient, objects: [ @@ -681,7 +681,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }; - await expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( `"Can't export more than 1 objects"` ); }); @@ -694,7 +694,7 @@ describe('getSortedObjectsForExport()', () => { objects: undefined, }; - expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( `"Either \`type\` or \`objects\` are required."` ); }); @@ -707,7 +707,7 @@ describe('getSortedObjectsForExport()', () => { search: 'foo', }; - expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( `"Can't specify both \\"search\\" and \\"objects\\" properties when exporting"` ); }); diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index 4b4cf1146aca01..a703c9f9fbd96c 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -124,7 +124,13 @@ async function fetchObjectsToExport({ } } -export async function getSortedObjectsForExport({ +/** + * Generates sorted saved object stream to be used for export. + * See the {@link SavedObjectsExportOptions | options} for more detailed information. + * + * @public + */ +export async function exportSavedObjectsToStream({ types, objects, search, diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 7533b8e5000391..37824cceb18cba 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -18,7 +18,7 @@ */ export { - getSortedObjectsForExport, + exportSavedObjectsToStream, SavedObjectsExportOptions, SavedObjectsExportResultDetails, } from './get_sorted_objects_for_export'; diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index f0719cbf4c829b..b43e5063c13e1e 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; -import { importSavedObjects } from './import_saved_objects'; +import { importSavedObjectsFromStream } from './import_saved_objects'; import { savedObjectsClientMock } from '../../mocks'; const emptyResponse = { @@ -76,7 +76,7 @@ describe('importSavedObjects()', () => { this.push(null); }, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 1, overwrite: false, @@ -103,7 +103,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -186,7 +186,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -270,7 +270,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: true, @@ -362,7 +362,7 @@ describe('importSavedObjects()', () => { references: [], })), }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -460,7 +460,7 @@ describe('importSavedObjects()', () => { }, ], }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -536,7 +536,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 5, overwrite: false, diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index ef3b4a214c2c29..cb1d70e5c8dc42 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -26,7 +26,13 @@ import { } from './types'; import { validateReferences } from './validate_references'; -export async function importSavedObjects({ +/** + * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more + * detailed information. + * + * @public + */ +export async function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index 95fa8aa192f3e8..e268e970b94acc 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -17,8 +17,8 @@ * under the License. */ -export { importSavedObjects } from './import_saved_objects'; -export { resolveImportErrors } from './resolve_import_errors'; +export { importSavedObjectsFromStream } from './import_saved_objects'; +export { resolveSavedObjectsImportErrors } from './resolve_import_errors'; export { SavedObjectsImportResponse, SavedObjectsImportError, diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index c522d76f1ff041..2c6d89e0a0a47b 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; -import { resolveImportErrors } from './resolve_import_errors'; +import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { savedObjectsClientMock } from '../../mocks'; describe('resolveImportErrors()', () => { @@ -80,7 +80,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [], }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [], @@ -107,7 +107,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: savedObjects.filter(obj => obj.type === 'visualization' && obj.id === '3'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ @@ -168,7 +168,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects.filter(obj => obj.type === 'index-pattern' && obj.id === '1'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ @@ -230,7 +230,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects.filter(obj => obj.type === 'dashboard' && obj.id === '4'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ @@ -312,7 +312,7 @@ describe('resolveImportErrors()', () => { references: [], })), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: savedObjects.map(obj => ({ @@ -415,7 +415,7 @@ describe('resolveImportErrors()', () => { }, ], }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 2, retries: [ @@ -503,7 +503,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [], }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 5, retries: [ @@ -547,7 +547,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects.filter(obj => obj.type === 'index-pattern' && obj.id === '1'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 6f56f283b4aec1..d9ac5678825730 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -27,7 +27,13 @@ import { } from './types'; import { validateReferences } from './validate_references'; -export async function resolveImportErrors({ +/** + * Resolve and return saved object import errors. + * See the {@link SavedObjectsResolveImportErrorsOptions | options} for more detailed informations. + * + * @public + */ +export async function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 44046378a7b97c..067579f54edac9 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -107,11 +107,17 @@ export interface SavedObjectsImportResponse { * @public */ export interface SavedObjectsImportOptions { + /** The stream of {@link SavedObject | saved objects} to import */ readStream: Readable; + /** The maximum number of object to import */ objectLimit: number; + /** if true, will override existing object if present */ overwrite: boolean; + /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; + /** the list of allowed types to import */ supportedTypes: string[]; + /** if specified, will import in given namespace, else will import as global object */ namespace?: string; } @@ -120,10 +126,16 @@ export interface SavedObjectsImportOptions { * @public */ export interface SavedObjectsResolveImportErrorsOptions { + /** The stream of {@link SavedObject | saved objects} to resolve errors from */ readStream: Readable; + /** The maximum number of object to import */ objectLimit: number; + /** client to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; + /** saved object import references to retry */ retries: SavedObjectsImportRetry[]; + /** the list of allowed types to import */ supportedTypes: string[]; + /** if specified, will import in given namespace */ namespace?: string; } diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 9bfe6580282581..661c6cbb79e58d 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -26,7 +26,7 @@ export { SavedObjectsManagement } from './management'; export * from './import'; export { - getSortedObjectsForExport, + exportSavedObjectsToStream, SavedObjectsExportOptions, SavedObjectsExportResultDetails, } from './export'; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index ab287332d8a65d..04d310681aec5a 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -26,7 +26,7 @@ import { } from '../../../../legacy/utils/streams'; import { IRouter } from '../../http'; import { SavedObjectConfig } from '../saved_objects_config'; -import { getSortedObjectsForExport } from '../export'; +import { exportSavedObjectsToStream } from '../export'; export const registerExportRoute = ( router: IRouter, @@ -67,7 +67,7 @@ export const registerExportRoute = ( router.handleLegacyErrors(async (context, req, res) => { const savedObjectsClient = context.core.savedObjects.client; const { type, objects, search, excludeExportDetails, includeReferencesDeep } = req.body; - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, types: typeof type === 'string' ? [type] : type, search, diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index e3f249dca05f7b..313e84c0b301d1 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -21,7 +21,7 @@ import { Readable } from 'stream'; import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { importSavedObjects } from '../import'; +import { importSavedObjectsFromStream } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; import { createSavedObjectsStreamFromNdJson } from './utils'; @@ -65,7 +65,7 @@ export const registerImportRoute = ( return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ supportedTypes, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index b52a8957176cc2..a81079b6825d62 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -18,7 +18,7 @@ */ jest.mock('../../export', () => ({ - getSortedObjectsForExport: jest.fn(), + exportSavedObjectsToStream: jest.fn(), })); import * as exportMock from '../../export'; @@ -30,7 +30,7 @@ import { registerExportRoute } from '../export'; import { setupServer } from './test_utils'; type setupServerReturn = UnwrapPromise>; -const getSortedObjectsForExport = exportMock.getSortedObjectsForExport as jest.Mock; +const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; const allowedTypes = ['index-pattern', 'search']; const config = { maxImportPayloadBytes: 10485760, @@ -76,7 +76,7 @@ describe('POST /api/saved_objects/_export', () => { ], }, ]; - getSortedObjectsForExport.mockResolvedValueOnce(createListStream(sortedObjects)); + exportSavedObjectsToStream.mockResolvedValueOnce(createListStream(sortedObjects)); const result = await supertest(httpSetup.server.listener) .post('/api/saved_objects/_export') @@ -96,7 +96,7 @@ describe('POST /api/saved_objects/_export', () => { const objects = (result.text as string).split('\n').map(row => JSON.parse(row)); expect(objects).toEqual(sortedObjects); - expect(getSortedObjectsForExport.mock.calls[0][0]).toEqual( + expect(exportSavedObjectsToStream.mock.calls[0][0]).toEqual( expect.objectContaining({ excludeExportDetails: false, exportSizeLimit: 10000, diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index efa7add7951b0d..a10a19ba1d8ff3 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -21,7 +21,7 @@ import { extname } from 'path'; import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { resolveImportErrors } from '../import'; +import { resolveSavedObjectsImportErrors } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; import { createSavedObjectsStreamFromNdJson } from './utils'; @@ -75,7 +75,7 @@ export const registerResolveImportErrorsRoute = ( if (fileExtension !== '.ndjson') { return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ supportedTypes, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index cbdff163245366..9fe32b14e64507 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -64,8 +64,11 @@ const createSetupContractMock = () => { setClientFactoryProvider: jest.fn(), addClientWrapper: jest.fn(), registerType: jest.fn(), + getImportExportObjectLimit: jest.fn(), }; + setupContract.getImportExportObjectLimit.mockReturnValue(100); + return setupContract; }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index d5a9f474209711..89f7990c771c87 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -154,6 +154,11 @@ export interface SavedObjectsServiceSetup { * This API is the single entry point to register saved object types in the new platform. */ registerType: (type: SavedObjectsType) => void; + + /** + * Returns the maximum number of objects allowed for import or export operations. + */ + getImportExportObjectLimit: () => number; } /** @@ -344,6 +349,7 @@ export class SavedObjectsService } this.typeRegistry.registerType(type); }, + getImportExportObjectLimit: () => this.config!.maxImportExportSize, }; } @@ -387,6 +393,14 @@ export class SavedObjectsService this.logger.info( 'Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...' ); + + // TODO: Move to Status Service https://github.com/elastic/kibana/issues/41983 + this.setupDeps!.elasticsearch.esNodesCompatibility$.subscribe(({ isCompatible, message }) => { + if (!isCompatible && message) { + this.logger.error(message); + } + }); + await this.setupDeps!.elasticsearch.esNodesCompatibility$.pipe( filter(nodes => nodes.isCompatible), take(1) diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 495d896ad12cd4..c9c672d0f8b1ce 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -62,7 +62,6 @@ export interface SavedObjectsMigrationVersion { } /** - * * @public */ export interface SavedObject { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 42bc1ce214b191..30695df33345ad 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -606,6 +606,8 @@ export interface CoreSetup { // (undocumented) http: HttpServiceSetup; // (undocumented) + metrics: MetricsServiceSetup; + // (undocumented) savedObjects: SavedObjectsServiceSetup; // (undocumented) uiSettings: UiSettingsServiceSetup; @@ -685,6 +687,9 @@ export interface DeprecationSettings { message: string; } +// @public +export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; + // @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; @@ -763,6 +768,9 @@ export interface ErrorHttpResponseOptions { headers?: ResponseHeaders; } +// @public +export function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; + // @public export interface FakeRequest { headers: Headers; @@ -891,6 +899,9 @@ export interface ImageValidation { }; } +// @public +export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; + // @public (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) @@ -1176,6 +1187,11 @@ export interface LogRecord { timestamp: Date; } +// @public +export interface MetricsServiceSetup { + getOpsMetrics$: () => Observable; +} + // @public (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; @@ -1227,6 +1243,63 @@ export interface OnPreResponseToolkit { next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; } +// @public +export interface OpsMetrics { + concurrent_connections: OpsServerMetrics['concurrent_connections']; + os: OpsOsMetrics; + process: OpsProcessMetrics; + requests: OpsServerMetrics['requests']; + response_times: OpsServerMetrics['response_times']; +} + +// @public +export interface OpsOsMetrics { + distro?: string; + distroRelease?: string; + load: { + '1m': number; + '5m': number; + '15m': number; + }; + memory: { + total_in_bytes: number; + free_in_bytes: number; + used_in_bytes: number; + }; + platform: NodeJS.Platform; + platformRelease: string; + uptime_in_millis: number; +} + +// @public +export interface OpsProcessMetrics { + event_loop_delay: number; + memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; + pid: number; + uptime_in_millis: number; +} + +// @public +export interface OpsServerMetrics { + concurrent_connections: number; + requests: { + disconnects: number; + total: number; + statusCodes: Record; + }; + response_times: { + avg_in_millis: number; + max_in_millis: number; + }; +} + // @public (undocumented) export interface PackageInfo { // (undocumented) @@ -1369,6 +1442,9 @@ export type RequestHandlerContextContainer = IContextContainer = IContextProvider, TContextName>; +// @public +export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; + // @public export type ResponseError = string | Error | { message: string | Error; @@ -1397,6 +1473,7 @@ export interface RouteConfigOptions { authRequired?: boolean; body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; + xsrfRequired?: Method extends 'get' ? never : boolean; } // @public @@ -1411,7 +1488,7 @@ export interface RouteConfigOptionsBody { export type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*'; // @public -export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; +export type RouteMethod = SafeRouteMethod | DestructiveRouteMethod; // @public export type RouteRegistrar = (route: RouteConfig, handler: RequestHandler) => void; @@ -1464,6 +1541,9 @@ export interface RouteValidatorOptions { }; } +// @public +export type SafeRouteMethod = 'get' | 'options'; + // @public (undocumented) export interface SavedObject { attributes: T; @@ -1827,17 +1907,11 @@ export interface SavedObjectsImportMissingReferencesError { // @public export interface SavedObjectsImportOptions { - // (undocumented) namespace?: string; - // (undocumented) objectLimit: number; - // (undocumented) overwrite: boolean; - // (undocumented) readStream: Readable; - // (undocumented) savedObjectsClient: SavedObjectsClientContract; - // (undocumented) supportedTypes: string[]; } @@ -1991,17 +2065,11 @@ export interface SavedObjectsRepositoryFactory { // @public export interface SavedObjectsResolveImportErrorsOptions { - // (undocumented) namespace?: string; - // (undocumented) objectLimit: number; - // (undocumented) readStream: Readable; - // (undocumented) retries: SavedObjectsImportRetry[]; - // (undocumented) savedObjectsClient: SavedObjectsClientContract; - // (undocumented) supportedTypes: string[]; } @@ -2032,6 +2100,7 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; + getImportExportObjectLimit: () => number; registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 038c4651ff5a7d..53d1b742a64944 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -79,3 +79,9 @@ export const mockUuidService = uuidServiceMock.create(); jest.doMock('./uuid/uuid_service', () => ({ UuidService: jest.fn(() => mockUuidService), })); + +import { metricsServiceMock } from './metrics/metrics_service.mock'; +export const mockMetricsService = metricsServiceMock.create(); +jest.doMock('./metrics/metrics_service', () => ({ + MetricsService: jest.fn(() => mockMetricsService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 161dd3759a218d..a4b5a9d81df203 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -28,6 +28,7 @@ import { mockEnsureValidConfiguration, mockUiSettingsService, mockRenderingService, + mockMetricsService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -61,6 +62,7 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -71,6 +73,7 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); expect(mockRenderingService.setup).toHaveBeenCalledTimes(1); + expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -107,6 +110,7 @@ test('runs services on "start"', async () => { expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); + expect(mockMetricsService.start).not.toHaveBeenCalled(); await server.start(); @@ -114,6 +118,7 @@ test('runs services on "start"', async () => { expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); + expect(mockMetricsService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -135,6 +140,7 @@ test('stops services on "stop"', async () => { expect(mockLegacyService.stop).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); + expect(mockMetricsService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -144,6 +150,7 @@ test('stops services on "stop"', async () => { expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); + expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -159,6 +166,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -178,4 +186,5 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index db2493b38d6e0a..8603f5fba1da87 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -34,6 +34,7 @@ import { Logger, LoggerFactory } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; +import { MetricsService, opsConfig } from './metrics'; import { config as cspConfig } from './csp'; import { config as elasticsearchConfig } from './elasticsearch'; @@ -67,6 +68,7 @@ export class Server { private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; + private readonly metrics: MetricsService; private coreStart?: InternalCoreStart; @@ -89,6 +91,7 @@ export class Server { this.uiSettings = new UiSettingsService(core); this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); + this.metrics = new MetricsService(core); } public async setup() { @@ -137,6 +140,8 @@ export class Server { legacyPlugins, }); + const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -145,6 +150,7 @@ export class Server { uiSettings: uiSettingsSetup, savedObjects: savedObjectsSetup, uuid: uuidSetup, + metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -193,6 +199,7 @@ export class Server { await this.http.start(); await this.rendering.start(); + await this.metrics.start(); return this.coreStart; } @@ -207,6 +214,7 @@ export class Server { await this.http.stop(); await this.uiSettings.stop(); await this.rendering.stop(); + await this.metrics.stop(); } private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) { @@ -260,6 +268,7 @@ export class Server { [savedObjectsConfig.path, savedObjectsConfig.schema], [savedObjectsMigrationConfig.path, savedObjectsMigrationConfig.schema], [uiSettingsConfig.path, uiSettingsConfig.schema], + [opsConfig.path, opsConfig.schema], ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); diff --git a/src/core/utils/merge.test.ts b/src/core/utils/merge.test.ts index c857e980dec21a..7ef07a83399ac4 100644 --- a/src/core/utils/merge.test.ts +++ b/src/core/utils/merge.test.ts @@ -17,6 +17,7 @@ * under the License. */ +// eslint-disable-next-line max-classes-per-file import { merge } from './merge'; describe('merge', () => { @@ -62,6 +63,29 @@ describe('merge', () => { expect(merge({ a: 0 }, { a: 1 }, {})).toEqual({ a: 1 }); }); + test('does not merge class instances', () => { + class Folder { + constructor(public readonly path: string) {} + getPath() { + return this.path; + } + } + class File { + constructor(public readonly content: string) {} + getContent() { + return this.content; + } + } + const folder = new Folder('/etc'); + const file = new File('yolo'); + + const result = merge({}, { content: folder }, { content: file }); + expect(result).toStrictEqual({ + content: file, + }); + expect(result.content.getContent()).toBe('yolo'); + }); + test(`doesn't pollute prototypes`, () => { merge({}, JSON.parse('{ "__proto__": { "foo": "bar" } }')); merge({}, JSON.parse('{ "constructor": { "prototype": { "foo": "bar" } } }')); diff --git a/src/core/utils/merge.ts b/src/core/utils/merge.ts index 8e5d9f4860d955..43878c27b1e199 100644 --- a/src/core/utils/merge.ts +++ b/src/core/utils/merge.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { isPlainObject } from 'lodash'; /** * Deeply merges two objects, omitting undefined values, and not deeply merging Arrays. * @@ -60,7 +60,7 @@ export function merge>( ) as TReturn; } -const isMergable = (obj: any) => typeof obj === 'object' && obj !== null && !Array.isArray(obj); +const isMergable = (obj: any) => isPlainObject(obj); const mergeObjects = , U extends Record>( baseObj: T, diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index fb91b865097faf..35ac4e27f9c8b3 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -20,6 +20,7 @@ export const storybookAliases = { apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', + codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', diff --git a/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.test.ts b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.test.ts new file mode 100644 index 00000000000000..bfba4d7f4c8dad --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.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 { + fieldFormats, + FieldFormatsGetConfigFn, + esFilters, + IndexPatternsContract, +} from '../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setIndexPatterns } from '../../../../../../plugins/data/public/services'; +import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; +import { createFiltersFromEvent, EventData } from './create_filters_from_event'; +import { mockDataServices } from '../../search/aggs/test_helpers'; + +jest.mock('ui/new_platform'); + +const mockField = { + name: 'bytes', + indexPattern: { + id: 'logstash-*', + }, + filterable: true, + format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), +}; + +describe('createFiltersFromEvent', () => { + let dataPoints: EventData[]; + + beforeEach(() => { + dataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'histogram', + indexPatternId: 'logstash-*', + aggConfigParams: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, + ]; + + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => ({ + id: 'logstash-*', + fields: { + getByName: () => mockField, + filter: () => [mockField], + }, + }), + } as unknown) as IndexPatternsContract); + }); + + test('ignores event when value for rows is not provided', async () => { + dataPoints[0].table.rows[0]['1-1'] = null; + const filters = await createFiltersFromEvent(dataPoints); + + expect(filters.length).toEqual(0); + }); + + test('handles an event when aggregations type is a terms', async () => { + if (dataPoints[0].table.columns[0].meta) { + dataPoints[0].table.columns[0].meta.type = 'terms'; + } + const filters = await createFiltersFromEvent(dataPoints); + + expect(filters.length).toEqual(1); + expect(filters[0].query.match_phrase.bytes).toEqual('2048'); + }); + + test('handles an event when aggregations type is not terms', async () => { + const filters = await createFiltersFromEvent(dataPoints); + + expect(filters.length).toEqual(1); + + const [rangeFilter] = filters; + + if (esFilters.isRangeFilter(rangeFilter)) { + expect(rangeFilter.range.bytes.gte).toEqual(2048); + expect(rangeFilter.range.bytes.lt).toEqual(2078); + } + }); +}); diff --git a/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.ts similarity index 70% rename from src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.ts index 1037c718d0003f..3713c781b0958d 100644 --- a/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.ts @@ -17,21 +17,33 @@ * under the License. */ -import { esFilters } from '../../../../../../plugins/data/public'; +import { KibanaDatatable } from '../../../../../../plugins/expressions/public'; +import { esFilters, Filter } from '../../../../../../plugins/data/public'; import { deserializeAggConfig } from '../../search/expressions/utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; +export interface EventData { + table: Pick; + column: number; + row: number; + value: any; +} + /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter * terms based on a specific cell in the tabified data. * - * @param {object} table - tabified table data + * @param {EventData['table']} table - tabified table data * @param {number} columnIndex - current column index * @param {number} rowIndex - current row index * @return {array} - array of terms to filter against */ -const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { +const getOtherBucketFilterTerms = ( + table: EventData['table'], + columnIndex: number, + rowIndex: number +) => { if (rowIndex === -1) { return []; } @@ -42,7 +54,7 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; }); }); - const terms = rows.map(row => row[table.columns[columnIndex].id]); + const terms: any[] = rows.map(row => row[table.columns[columnIndex].id]); return [ ...new Set( @@ -59,22 +71,27 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * Assembles the filters needed to apply filtering against a specific cell value, while accounting * for cases like if the value is a terms agg in an `__other__` or `__missing__` bucket. * - * @param {object} table - tabified table data + * @param {EventData['table']} table - tabified table data * @param {number} columnIndex - current column index * @param {number} rowIndex - current row index * @param {string} cellValue - value of the current cell - * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() + * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() */ -const createFilter = async (table, columnIndex, rowIndex) => { - if (!table || !table.columns || !table.columns[columnIndex]) return; +const createFilter = async (table: EventData['table'], columnIndex: number, rowIndex: number) => { + if (!table || !table.columns || !table.columns[columnIndex]) { + return; + } const column = table.columns[columnIndex]; + if (!column.meta || !column.meta.indexPatternId) { + return; + } const aggConfig = deserializeAggConfig({ type: column.meta.type, - aggConfigParams: column.meta.aggConfigParams, + aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), }); - let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; + let filter: Filter[] = []; + const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,6 +102,10 @@ const createFilter = async (table, columnIndex, rowIndex) => { filter = aggConfig.createFilter(value); } + if (!filter) { + return; + } + if (!Array.isArray(filter)) { filter = [filter]; } @@ -92,19 +113,18 @@ const createFilter = async (table, columnIndex, rowIndex) => { return filter; }; -const createFiltersFromEvent = async event => { - const filters = []; - const dataPoints = event.data || [event]; +const createFiltersFromEvent = async (dataPoints: EventData[], negate?: boolean) => { + const filters: Filter[] = []; await Promise.all( dataPoints .filter(point => point) .map(async val => { const { table, column, row } = val; - const filter = await createFilter(table, column, row); + const filter: Filter[] = (await createFilter(table, column, row)) || []; if (filter) { filter.forEach(f => { - if (event.negate) { + if (negate) { f = esFilters.toggleFilterNegated(f); } filters.push(f); diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 7f1c5d78ab8006..21046f8bb834fb 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -19,21 +19,21 @@ import { i18n } from '@kbn/i18n'; import { - Action, createAction, IncompatibleActionError, + ActionByType, } from '../../../../../plugins/ui_actions/public'; import { onBrushEvent } from './filters/brush_event'; import { FilterManager, TimefilterContract, esFilters } from '../../../../../plugins/data/public'; -export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; +export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -interface ActionContext { +export interface SelectRangeActionContext { data: any; timeFieldName: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: SelectRangeActionContext) { try { return Boolean(await onBrushEvent(context.data)); } catch { @@ -44,17 +44,17 @@ async function isCompatible(context: ActionContext) { export function selectRangeAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ - type: SELECT_RANGE_ACTION, - id: SELECT_RANGE_ACTION, +): ActionByType { + return createAction({ + type: ACTION_SELECT_RANGE, + id: ACTION_SELECT_RANGE, getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ActionContext) => { + execute: async ({ timeFieldName, data }: SelectRangeActionContext) => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); } diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts index 260b401e6d6581..4c69bc82629227 100644 --- a/src/legacy/core_plugins/data/public/actions/value_click_action.ts +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../plugins/kibana_react/public'; import { - Action, + ActionByType, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -37,16 +37,18 @@ import { esFilters, } from '../../../../../plugins/data/public'; -export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; +export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -interface ActionContext { +export interface ValueClickActionContext { data: any; timeFieldName: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: ValueClickActionContext) { try { - const filters: Filter[] = (await createFiltersFromEvent(context.data)) || []; + const filters: Filter[] = + (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || + []; return filters.length > 0; } catch { return false; @@ -56,22 +58,23 @@ async function isCompatible(context: ActionContext) { export function valueClickAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ - type: VALUE_CLICK_ACTION, - id: VALUE_CLICK_ACTION, +): ActionByType { + return createAction({ + type: ACTION_VALUE_CLICK, + id: ACTION_VALUE_CLICK, getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ActionContext) => { + execute: async ({ timeFieldName, data }: ValueClickActionContext) => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); } - const filters: Filter[] = (await createFiltersFromEvent(data)) || []; + const filters: Filter[] = + (await createFiltersFromEvent(data.data || [data], data.negate)) || []; let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index e2b8ca5dda78cf..18230646ab412d 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -37,8 +37,16 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; import { setSearchServiceShim } from './services'; -import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; -import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + selectRangeAction, + SelectRangeActionContext, + ACTION_SELECT_RANGE, +} from './actions/select_range_action'; +import { + valueClickAction, + ACTION_VALUE_CLICK, + ValueClickActionContext, +} from './actions/value_click_action'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, @@ -76,6 +84,12 @@ export interface DataSetup { export interface DataStart { search: SearchStart; } +declare module '../../../../plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_SELECT_RANGE]: SelectRangeActionContext; + [ACTION_VALUE_CLICK]: ValueClickActionContext; + } +} /** * Data Plugin - public @@ -100,10 +114,13 @@ export class DataPlugin // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); - uiActions.registerAction( + uiActions.attachAction( + SELECT_RANGE_TRIGGER, selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) ); - uiActions.registerAction( + + uiActions.attachAction( + VALUE_CLICK_TRIGGER, valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) ); @@ -123,9 +140,6 @@ export class DataPlugin setSearchService(data.search); setOverlays(core.overlays); - uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); - uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); - return { search, }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts similarity index 54% rename from src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 749dad377f2e25..976ab57c00b631 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -17,39 +17,73 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket, -} from '../../buckets/_terms_other_bucket_helper'; -import { start as visualizationsStart } from '../../../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +} from './_terms_other_bucket_helper'; +import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; +import { BUCKET_TYPES } from './bucket_agg_types'; +import { IBucketAggConfig } from './_bucket_agg_type'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; -const visConfigSingleTerm = { - type: 'pie', +const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'field', + }, + ], +} as any; + +const singleTerm = { aggs: [ { - type: 'terms', - schema: 'segment', - params: { field: 'machine.os.raw', otherBucket: true, missingBucket: true }, + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + otherBucket: true, + missingBucket: true, + }, }, ], }; -const visConfigNestedTerm = { - type: 'pie', +const nestedTerm = { aggs: [ { - type: 'terms', - schema: 'segment', - params: { field: 'geo.src', size: 2, otherBucket: false, missingBucket: false }, + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'geo.src', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: false, + missingBucket: false, + }, }, { - type: 'terms', - schema: 'segment', - params: { field: 'machine.os.raw', size: 2, otherBucket: true, missingBucket: true }, + id: '2', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: true, + missingBucket: true, + }, }, ], }; @@ -183,28 +217,36 @@ const nestedOtherResponse = { status: 200, }; -describe('Terms Agg Other bucket helper', () => { - let vis; +jest.mock('ui/new_platform'); - function init(aggConfig) { - ngMock.module('kibana'); - ngMock.inject(Private => { - const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); +describe('Terms Agg Other bucket helper', () => { + const typesRegistry = mockAggTypesRegistry(); + const getAggConfigs = (aggs: CreateAggConfigParams[] = []) => { + return new AggConfigs(indexPattern, [...aggs], { typesRegistry }); + }; - vis = new visualizationsStart.Vis(indexPattern, aggConfig); - }); - } + beforeEach(() => { + mockDataServices(); + }); describe('buildOtherBucketAgg', () => { - it('returns a function', () => { - init(visConfigSingleTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse); - expect(agg).to.be.a('function'); + test('returns a function', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse + ); + expect(typeof agg).toBe('function'); }); - it('correctly builds query with single terms agg', () => { - init(visConfigSingleTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse)(); + test('correctly builds query with single terms agg', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse + ); const expectedResponse = { aggs: undefined, filters: { @@ -223,13 +265,19 @@ describe('Terms Agg Other bucket helper', () => { }, }, }; - - expect(agg['other-filter']).to.eql(expectedResponse); + expect(agg).toBeDefined(); + if (agg) { + expect(agg()['other-filter']).toEqual(expectedResponse); + } }); - it('correctly builds query for nested terms agg', () => { - init(visConfigNestedTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponse)(); + test('correctly builds query for nested terms agg', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponse + ); const expectedResponse = { 'other-filter': { aggs: undefined, @@ -267,54 +315,84 @@ describe('Terms Agg Other bucket helper', () => { }, }, }; - - expect(agg).to.eql(expectedResponse); + expect(agg).toBeDefined(); + if (agg) { + expect(agg()).toEqual(expectedResponse); + } }); - it('returns false when nested terms agg has no buckets', () => { - init(visConfigNestedTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponseNoResults); - expect(agg).to.eql(false); + test('returns false when nested terms agg has no buckets', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponseNoResults + ); + + expect(agg).toEqual(false); }); }); describe('mergeOtherBucketAggResponse', () => { - it('correctly merges other bucket with single terms agg', () => { - init(visConfigSingleTerm); - const otherAggConfig = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse)(); - const mergedResponse = mergeOtherBucketAggResponse( - vis.aggs, - singleTermResponse, - singleOtherResponse, - vis.aggs.aggs[0], - otherAggConfig + test('correctly merges other bucket with single terms agg', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const otherAggConfig = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse ); - expect(mergedResponse.aggregations['1'].buckets[3].key).to.equal('__other__'); + expect(otherAggConfig).toBeDefined(); + if (otherAggConfig) { + const mergedResponse = mergeOtherBucketAggResponse( + aggConfigs, + singleTermResponse, + singleOtherResponse, + aggConfigs.aggs[0] as IBucketAggConfig, + otherAggConfig() + ); + expect(mergedResponse.aggregations['1'].buckets[3].key).toEqual('__other__'); + } }); - it('correctly merges other bucket with nested terms agg', () => { - init(visConfigNestedTerm); - const otherAggConfig = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponse)(); - const mergedResponse = mergeOtherBucketAggResponse( - vis.aggs, - nestedTermResponse, - nestedOtherResponse, - vis.aggs.aggs[1], - otherAggConfig + test('correctly merges other bucket with nested terms agg', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const otherAggConfig = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponse ); - expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).to.equal('__other__'); + expect(otherAggConfig).toBeDefined(); + if (otherAggConfig) { + const mergedResponse = mergeOtherBucketAggResponse( + aggConfigs, + nestedTermResponse, + nestedOtherResponse, + aggConfigs.aggs[1] as IBucketAggConfig, + otherAggConfig() + ); + + expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).toEqual( + '__other__' + ); + } }); }); describe('updateMissingBucket', () => { - it('correctly updates missing bucket key', () => { - init(visConfigNestedTerm); - const updatedResponse = updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs.aggs[0]); + test('correctly updates missing bucket key', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const updatedResponse = updateMissingBucket( + singleTermResponse, + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig + ); expect( - updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === '__missing__') - ).to.not.be('undefined'); + updatedResponse.aggregations['1'].buckets.find( + (bucket: Record) => bucket.key === '__missing__' + ) + ).toBeDefined(); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts similarity index 65% rename from src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts index ddab360161744c..42db37c81eadd6 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -17,21 +17,24 @@ * under the License. */ -import _ from 'lodash'; +import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash'; import { esFilters, esQuery } from '../../../../../../../plugins/data/public'; import { AggGroupNames } from '../agg_groups'; +import { IAggConfigs } from '../agg_configs'; +import { IBucketAggConfig } from './_bucket_agg_type'; /** * walks the aggregation DSL and returns DSL starting at aggregation with id of startFromAggId * @param aggNestedDsl: aggregation config DSL (top level) * @param startFromId: id of an aggregation from where we want to get the nested DSL */ -const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { +const getNestedAggDSL = (aggNestedDsl: Record, startFromAggId: string): any => { if (aggNestedDsl[startFromAggId]) { return aggNestedDsl[startFromAggId]; } - const nestedAggs = _.values(aggNestedDsl); + const nestedAggs: Array> = values(aggNestedDsl); let aggs; + for (let i = 0; i < nestedAggs.length; i++) { if (nestedAggs[i].aggs && (aggs = getNestedAggDSL(nestedAggs[i].aggs, startFromAggId))) { return aggs; @@ -46,27 +49,34 @@ const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { * @param aggWithOtherBucket: AggConfig of the aggregation with other bucket enabled * @param key: key from the other bucket request for a specific other bucket */ -const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { +const getAggResultBuckets = ( + aggConfigs: IAggConfigs, + response: any, + aggWithOtherBucket: IBucketAggConfig, + key: string +) => { const keyParts = key.split('-'); let responseAgg = response; for (const i in keyParts) { if (keyParts[i]) { - const responseAggs = _.values(responseAgg); + const responseAggs: Array> = values(responseAgg); // If you have multi aggs, we cannot just assume the first one is the `other` bucket, // so we need to loop over each agg until we find it. for (let aggId = 0; aggId < responseAggs.length; aggId++) { - const agg = responseAggs[aggId]; - const aggKey = _.keys(responseAgg)[aggId]; - const aggConfig = _.find(aggConfigs.aggs, agg => agg.id === aggKey); - const bucket = _.find(agg.buckets, (bucket, bucketObjKey) => { - const bucketKey = aggConfig - .getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey) - .toString(); - return bucketKey === keyParts[i]; - }); - if (bucket) { - responseAgg = bucket; - break; + const aggById = responseAggs[aggId]; + const aggKey = keys(responseAgg)[aggId]; + const aggConfig = find(aggConfigs.aggs, agg => agg.id === aggKey); + if (aggConfig) { + const aggResultBucket = find(aggById.buckets, (bucket, bucketObjKey) => { + const bucketKey = aggConfig + .getKey(bucket, isNumber(bucketObjKey) ? undefined : bucketObjKey) + .toString(); + return bucketKey === keyParts[i]; + }); + if (aggResultBucket) { + responseAgg = aggResultBucket; + break; + } } } } @@ -82,21 +92,20 @@ const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { * @param responseAggs: array of aggregations from response * @param aggId: id of the aggregation with missing bucket */ -const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { +const getAggConfigResultMissingBuckets = (responseAggs: any, aggId: string) => { const missingKey = '__missing__'; - let resultBuckets = []; + let resultBuckets: Array> = []; if (responseAggs[aggId]) { - const matchingBucket = responseAggs[aggId].buckets.find(bucket => bucket.key === missingKey); + const matchingBucket = responseAggs[aggId].buckets.find( + (bucket: Record) => bucket.key === missingKey + ); if (matchingBucket) resultBuckets.push(matchingBucket); return resultBuckets; } - _.each(responseAggs, agg => { + each(responseAggs, agg => { if (agg.buckets) { - _.each(agg.buckets, bucket => { - resultBuckets = [ - ...resultBuckets, - ...getAggConfigResultMissingBuckets(bucket, aggId, missingKey), - ]; + each(agg.buckets, bucket => { + resultBuckets = [...resultBuckets, ...getAggConfigResultMissingBuckets(bucket, aggId)]; }); } }); @@ -110,13 +119,24 @@ const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { * @param key: the key for this specific other bucket * @param otherAgg: AggConfig of the aggregation with other bucket */ -const getOtherAggTerms = (requestAgg, key, otherAgg) => { +const getOtherAggTerms = ( + requestAgg: Record, + key: string, + otherAgg: IBucketAggConfig +) => { return requestAgg['other-filter'].filters.filters[key].bool.must_not - .filter(filter => filter.match_phrase && filter.match_phrase[otherAgg.params.field.name]) - .map(filter => filter.match_phrase[otherAgg.params.field.name]); + .filter( + (filter: Record) => + filter.match_phrase && filter.match_phrase[otherAgg.params.field.name] + ) + .map((filter: Record) => filter.match_phrase[otherAgg.params.field.name]); }; -export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => { +export const buildOtherBucketAgg = ( + aggConfigs: IAggConfigs, + aggWithOtherBucket: IBucketAggConfig, + response: any +) => { const bucketAggs = aggConfigs.aggs.filter(agg => agg.type.type === AggGroupNames.Buckets); const index = bucketAggs.findIndex(agg => agg.id === aggWithOtherBucket.id); const aggs = aggConfigs.toDsl(); @@ -130,6 +150,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => params: { filters: [], }, + enabled: false, }, { addToAggConfigs: false, @@ -145,25 +166,31 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => let noAggBucketResults = false; // recursively create filters for all parent aggregation buckets - const walkBucketTree = (aggIndex, aggs, aggId, filters, key) => { + const walkBucketTree = ( + aggIndex: number, + aggregations: any, + aggId: string, + filters: any[], + key: string + ) => { // make sure there are actually results for the buckets - if (aggs[aggId].buckets.length < 1) { + if (aggregations[aggId].buckets.length < 1) { noAggBucketResults = true; return; } - const agg = aggs[aggId]; + const agg = aggregations[aggId]; const newAggIndex = aggIndex + 1; const newAgg = bucketAggs[newAggIndex]; const currentAgg = bucketAggs[aggIndex]; if (aggIndex < index) { - _.each(agg.buckets, (bucket, bucketObjKey) => { + each(agg.buckets, (bucket: any, bucketObjKey) => { const bucketKey = currentAgg.getKey( bucket, - Number.isInteger(bucketObjKey) ? null : bucketObjKey + isNumber(bucketObjKey) ? undefined : bucketObjKey ); - const filter = _.cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); - const newFilters = _.flatten([...filters, filter]); + const filter = cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); + const newFilters = flatten([...filters, filter]); walkBucketTree( newAggIndex, bucket, @@ -177,7 +204,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => if ( !aggWithOtherBucket.params.missingBucket || - agg.buckets.some(bucket => bucket.key === '__missing__') + agg.buckets.some((bucket: { key: string }) => bucket.key === '__missing__') ) { filters.push( esFilters.buildExistsFilter( @@ -188,7 +215,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => } // create not filters for all the buckets - _.each(agg.buckets, bucket => { + each(agg.buckets, bucket => { if (bucket.key === '__missing__') return; const filter = currentAgg.createFilter(bucket.key); filter.meta.negate = true; @@ -214,15 +241,15 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => }; export const mergeOtherBucketAggResponse = ( - aggsConfig, - response, - otherResponse, - otherAgg, - requestAgg + aggsConfig: IAggConfigs, + response: any, + otherResponse: any, + otherAgg: IBucketAggConfig, + requestAgg: Record ) => { - const updatedResponse = _.cloneDeep(response); - _.each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { - if (!bucket.doc_count) return; + const updatedResponse = cloneDeep(response); + each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { + if (!bucket.doc_count || key === undefined) return; const bucketKey = key.replace(/^-/, ''); const aggResultBuckets = getAggResultBuckets( aggsConfig, @@ -241,7 +268,11 @@ export const mergeOtherBucketAggResponse = ( bucket.filters = [phraseFilter]; bucket.key = '__other__'; - if (aggResultBuckets.some(bucket => bucket.key === '__missing__')) { + if ( + aggResultBuckets.some( + (aggResultBucket: Record) => aggResultBucket.key === '__missing__' + ) + ) { bucket.filters.push( esFilters.buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern) ); @@ -251,8 +282,12 @@ export const mergeOtherBucketAggResponse = ( return updatedResponse; }; -export const updateMissingBucket = (response, aggConfigs, agg) => { - const updatedResponse = _.cloneDeep(response); +export const updateMissingBucket = ( + response: any, + aggConfigs: IAggConfigs, + agg: IBucketAggConfig +) => { + const updatedResponse = cloneDeep(response); const aggResultBuckets = getAggConfigResultMissingBuckets(updatedResponse.aggregations, agg.id); aggResultBuckets.forEach(bucket => { bucket.key = '__missing__'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts index 0ed44aa8767442..8fd95c86d8476d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts @@ -39,7 +39,6 @@ import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket, - // @ts-ignore } from './_terms_other_bucket_helper'; import { Schemas } from '../schemas'; import { AggGroupNames } from '../agg_groups'; diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index e85e9deff6ddf9..bd05fa21bfd5de 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -20,7 +20,7 @@ import { set } from 'lodash'; // @ts-ignore import { FormattedData } from '../../../../../../plugins/inspector/public'; -// @ts-ignore + import { createFilter } from './create_filter'; import { TabbedTable } from '../tabify'; @@ -66,7 +66,10 @@ export async function buildTabularInspectorData( row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw ); const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); - queryFilter.addFilters(filter); + + if (filter) { + queryFilter.addFilters(filter); + } }), filterOut: isCellContentFilterable && @@ -75,14 +78,17 @@ export async function buildTabularInspectorData( row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw ); const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); - const notOther = value.raw !== '__other__'; - const notMissing = value.raw !== '__missing__'; - if (Array.isArray(filter)) { - filter.forEach(f => set(f, 'meta.negate', notOther && notMissing)); - } else { - set(filter, 'meta.negate', notOther && notMissing); + + if (filter) { + const notOther = value.raw !== '__other__'; + const notMissing = value.raw !== '__missing__'; + if (Array.isArray(filter)) { + filter.forEach(f => set(f, 'meta.negate', notOther && notMissing)); + } else { + set(filter, 'meta.negate', notOther && notMissing); + } + queryFilter.addFilters(filter); } - queryFilter.addFilters(filter); }), }; }); diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.test.ts b/src/legacy/core_plugins/data/public/search/expressions/create_filter.test.ts new file mode 100644 index 00000000000000..890ec81778d4bf --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { + fieldFormats, + FieldFormatsGetConfigFn, + esFilters, +} from '../../../../../../plugins/data/public'; +import { createFilter } from './create_filter'; +import { TabbedTable } from '../tabify'; +import { AggConfigs } from '../aggs/agg_configs'; +import { IAggConfig } from '../aggs/agg_config'; +import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; + +describe('createFilter', () => { + let table: TabbedTable; + let aggConfig: IAggConfig; + + const typesRegistry = mockAggTypesRegistry(); + + const getAggConfigs = (type: string, params: any) => { + const field = { + name: 'bytes', + filterable: true, + indexPattern: { + id: '1234', + }, + format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new AggConfigs( + indexPattern, + [ + { + id: type, + type, + schema: 'buckets', + params, + }, + ], + { typesRegistry } + ); + }; + + const aggConfigParams: Record = { + field: 'bytes', + interval: 30, + otherBucket: true, + }; + + beforeEach(() => { + table = { + columns: [ + { + id: '1-1', + name: 'test', + aggConfig, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }; + mockDataServices(); + }); + + test('ignores event when cell value is not provided', async () => { + aggConfig = getAggConfigs('histogram', aggConfigParams).aggs[0]; + const filters = await createFilter([aggConfig], table, 0, -1, null); + + expect(filters).not.toBeDefined(); + }); + + test('handles an event when aggregations type is a terms', async () => { + aggConfig = getAggConfigs('terms', aggConfigParams).aggs[0]; + const filters = await createFilter([aggConfig], table, 0, 0, 'test'); + + expect(filters).toBeDefined(); + + if (filters) { + expect(filters.length).toEqual(1); + expect(filters[0].query.match_phrase.bytes).toEqual('2048'); + } + }); + + test('handles an event when aggregations type is not terms', async () => { + aggConfig = getAggConfigs('histogram', aggConfigParams).aggs[0]; + const filters = await createFilter([aggConfig], table, 0, 0, 'test'); + + expect(filters).toBeDefined(); + + if (filters) { + expect(filters.length).toEqual(1); + + const [rangeFilter] = filters; + + if (esFilters.isRangeFilter(rangeFilter)) { + expect(rangeFilter.range.bytes.gte).toEqual(2048); + expect(rangeFilter.range.bytes.lt).toEqual(2078); + } + } + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.ts similarity index 78% rename from src/legacy/core_plugins/data/public/search/expressions/create_filter.js rename to src/legacy/core_plugins/data/public/search/expressions/create_filter.ts index 3f4028a9b5525b..77e011932195ca 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.ts @@ -17,7 +17,11 @@ * under the License. */ -const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { +import { IAggConfig } from 'ui/agg_types'; +import { Filter } from '../../../../../../plugins/data/public'; +import { TabbedTable } from '../tabify'; + +const getOtherBucketFilterTerms = (table: TabbedTable, columnIndex: number, rowIndex: number) => { if (rowIndex === -1) { return []; } @@ -41,11 +45,17 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { ]; }; -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = ( + aggConfigs: IAggConfig[], + table: TabbedTable, + columnIndex: number, + rowIndex: number, + cellValue: any +) => { const column = table.columns[columnIndex]; const aggConfig = aggConfigs[columnIndex]; - let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + let filter: Filter[] = []; + const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -56,6 +66,10 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { filter = aggConfig.createFilter(value); } + if (!filter) { + return; + } + if (!Array.isArray(filter)) { filter = [filter]; } diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 99482a4be2d7b4..59ae99260cecd5 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -25,6 +25,7 @@ exports[`renders ListControl 1`] = ` compressed={false} data-test-subj="listControlSelect0" fullWidth={false} + inputRef={[Function]} isClearable={true} isLoading={false} onChange={[Function]} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx index d62adfdce56b47..d01cef15ea41bb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx @@ -58,8 +58,17 @@ class ListControlUi extends PureComponent { + if (this.textInput) { + this.textInput.setAttribute('focusable', 'false'); // remove when #59039 is fixed + } this.isMounted = true; }; @@ -67,6 +76,10 @@ class ListControlUi extends PureComponent { + this.textInput = ref; + }; + handleOnChange = (selectedOptions: any[]) => { const selectedValues = selectedOptions.map(({ value }) => { return value; @@ -143,6 +156,7 @@ class ListControlUi extends PureComponent ); } diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js index b3ee0a8fa7b048..e7171a5291d26b 100644 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ b/src/legacy/core_plugins/kibana/public/.eslintrc.js @@ -77,7 +77,7 @@ module.exports = { { basePath: path.resolve(__dirname, '../../../../../'), zones: topLevelRestricedZones.concat( - buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools', 'home']) + buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools']) ), }, ], diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap index f5a00e5435ed6c..771d53b73d9601 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap @@ -28,6 +28,7 @@ exports[`renders DashboardCloneModal 1`] = ` } showCopyOnSave={true} + showDescription={false} title="dash title" /> `; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/clone_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/clone_modal.tsx index e5e75e4b7d2771..08e2b98d1c73d9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/clone_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/clone_modal.tsx @@ -178,6 +178,9 @@ export class DashboardCloneModal extends React.Component { { showCopyOnSave={this.props.showCopyOnSave} objectType="dashboard" options={this.renderDashboardSaveOptions()} + showDescription={false} /> ); } diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/README.md b/src/legacy/core_plugins/kibana/public/dev_tools/README.md index 0234830d6071c0..199ddcc754d041 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/README.md +++ b/src/legacy/core_plugins/kibana/public/dev_tools/README.md @@ -1,4 +1,3 @@ This folder is just a left-over of the things that can't be moved to Kibana platform just yet: -* Styling (this can be moved as soon as there is support for styling in Kibana platform) * Check whether there are no dev tools and hide the link in the nav bar (this can be moved as soon as all dev tools are moved) \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index 6b0d2368cc1a25..c58307adaf38c6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -33,11 +33,10 @@ import { import { DiscoverStartPlugins } from './plugin'; import { SharePluginStart } from '../../../../../plugins/share/public'; -import { SavedSearch } from './np_ready/types'; import { DocViewsRegistry } from './np_ready/doc_views/doc_views_registry'; import { ChartsPluginStart } from '../../../../../plugins/charts/public'; import { VisualizationsStart } from '../../../visualizations/public'; -import { createSavedSearchesLoader } from '../../../../../plugins/discover/public'; +import { createSavedSearchesLoader, SavedSearch } from '../../../../../plugins/discover/public'; export interface DiscoverServices { addBasePath: (path: string) => string; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index 8fb6140d55e319..bf185f78941de2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -24,9 +24,9 @@ import { syncStates, BaseStateContainer, } from '../../../../../../../plugins/kibana_utils/public'; -import { esFilters, FilterManager, Filter } from '../../../../../../../plugins/data/public'; +import { esFilters, FilterManager, Filter, Query } from '../../../../../../../plugins/data/public'; -interface AppState { +export interface AppState { /** * Columns displayed in the table, cannot be changed by UI, just in discover's main app */ @@ -47,6 +47,7 @@ interface AppState { * Number of records to be fetched after the anchor records (older records) */ successorCount: number; + query?: Query; } interface GlobalState { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 1ac54ad5dabeef..bb693ab860221c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -305,6 +305,7 @@ function discoverController( defaultMessage: 'Save your Discover search so you can use it in visualizations and dashboards', })} + showDescription={false} /> ); showSaveModal(saveModal, core.i18n.Context); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap index a6aab8f74a6746..20e503fd5ff915 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap @@ -5,6 +5,7 @@ exports[`it renders ToolBarPagerButtons 1`] = ` className="kuiButtonGroup" > @@ -41,6 +48,12 @@ export function ToolBarPagerButtons(props: Props) { onClick={() => props.onPageNext()} disabled={!props.hasNextPage} data-test-subj="btnNextPage" + aria-label={i18n.translate( + 'kbn.discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', + { + defaultMessage: 'Next page in table', + } + )} > diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts index 2bbeea9d675c7e..100d9cdac133b5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts @@ -23,9 +23,9 @@ import { get } from 'lodash'; export function getPainlessError(error: Error) { const rootCause: Array<{ lang: string; script: string }> | undefined = get( error, - 'resp.error.root_cause' + 'body.attributes.error.root_cause' ); - const message: string = get(error, 'message'); + const message: string = get(error, 'body.message'); if (!rootCause) { return; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js index a175a1aebebdfa..df970ab5f25843 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js @@ -24,7 +24,11 @@ import './discover_field'; import './discover_field_search_directive'; import './discover_index_pattern_directive'; import fieldChooserTemplate from './field_chooser.html'; -import { IndexPatternFieldList } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternFieldList, + KBN_FIELD_TYPES, +} from '../../../../../../../../plugins/data/public'; +import { getMapsAppUrl, isFieldVisualizable, isMapsAppRegistered } from './lib/visualize_url_utils'; export function createFieldChooserDirective($location, config, $route) { return { @@ -186,8 +190,15 @@ export function createFieldChooserDirective($location, config, $route) { return ''; } + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return getMapsAppUrl(field, $scope.indexPattern, $scope.state, $scope.columns); + } + let agg = {}; - const isGeoPoint = field.type === 'geo_point'; + const isGeoPoint = field.type === KBN_FIELD_TYPES.GEO_POINT; const type = isGeoPoint ? 'tile_map' : 'histogram'; // If we're visualizing a date field, and our index is time based (and thus has a time filter), // then run a date histogram @@ -243,7 +254,7 @@ export function createFieldChooserDirective($location, config, $route) { $scope.computeDetails = function(field, recompute) { if (_.isUndefined(field.details) || recompute) { field.details = { - visualizeUrl: field.visualizable ? getVisualizeUrl(field) : null, + visualizeUrl: isFieldVisualizable(field) ? getVisualizeUrl(field) : null, ...fieldCalculator.getFieldValueCounts({ hits: $scope.hits, field: field, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html index 5d134911fc91b7..333dc472e956d8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html @@ -79,7 +79,7 @@ @@ -87,7 +87,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts new file mode 100644 index 00000000000000..8dbf3cd79ccb16 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts @@ -0,0 +1,108 @@ +/* + * 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 uuid from 'uuid/v4'; +// @ts-ignore +import rison from 'rison-node'; +import { + IFieldType, + IIndexPattern, + KBN_FIELD_TYPES, +} from '../../../../../../../../../plugins/data/public'; +import { AppState } from '../../../angular/context_state'; +import { getServices } from '../../../../kibana_services'; + +function getMapsAppBaseUrl() { + const mapsAppVisAlias = getServices() + .visualizations.types.getAliases() + .find(({ name }) => { + return name === 'maps'; + }); + return mapsAppVisAlias ? mapsAppVisAlias.aliasUrl : null; +} + +export function isMapsAppRegistered() { + return getServices() + .visualizations.types.getAliases() + .some(({ name }) => { + return name === 'maps'; + }); +} + +export function isFieldVisualizable(field: IFieldType) { + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return true; + } + return field.visualizable; +} + +export function getMapsAppUrl( + field: IFieldType, + indexPattern: IIndexPattern, + appState: AppState, + columns: string[] +) { + const mapAppParams = new URLSearchParams(); + + // Copy global state + const locationSplit = window.location.href.split('discover?'); + if (locationSplit.length > 1) { + const discoverParams = new URLSearchParams(locationSplit[1]); + const globalStateUrlValue = discoverParams.get('_g'); + if (globalStateUrlValue) { + mapAppParams.set('_g', globalStateUrlValue); + } + } + + // Copy filters and query in app state + const mapsAppState: any = { + filters: appState.filters || [], + }; + if (appState.query) { + mapsAppState.query = appState.query; + } + // @ts-ignore + mapAppParams.set('_a', rison.encode(mapsAppState)); + + // create initial layer descriptor + const hasColumns = columns && columns.length && columns[0] !== '_source'; + mapAppParams.set( + 'initialLayers', + // @ts-ignore + rison.encode_array([ + { + id: uuid(), + label: indexPattern.title, + sourceDescriptor: { + id: uuid(), + type: 'ES_SEARCH', + geoField: field.name, + tooltipProperties: hasColumns ? columns : [], + indexPatternId: indexPattern.id, + }, + visible: true, + type: 'VECTOR', + }, + ]) + ); + + return getServices().addBasePath(`${getMapsAppBaseUrl()}?${mapAppParams.toString()}`); +} diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts index 738a74d93449d0..0aaf3e7f156c13 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts @@ -37,7 +37,6 @@ import { Embeddable, } from '../../../../../embeddable_api/public/np_ready/public'; import * as columnActions from '../angular/doc_table/actions/columns'; -import { SavedSearch } from '../types'; import searchTemplate from './search_template.html'; import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; @@ -51,6 +50,7 @@ import { ISearchSource, } from '../../kibana_services'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +import { SavedSearch } from '../../../../../../../plugins/discover/public'; interface SearchScope extends ng.IScope { columns?: string[]; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts index e7aa390cda858f..b20e9b2faf7c49 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts @@ -18,9 +18,9 @@ */ import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from 'src/plugins/embeddable/public'; -import { SavedSearch } from '../types'; import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { Filter, IIndexPattern, TimeRange, Query } from '../../../../../../../plugins/data/public'; +import { SavedSearch } from '../../../../../../../plugins/discover/public'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/home/_index.scss b/src/legacy/core_plugins/kibana/public/home/_index.scss deleted file mode 100644 index f42254c1096ce6..00000000000000 --- a/src/legacy/core_plugins/kibana/public/home/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'np_ready/components/index'; diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts deleted file mode 100644 index f8c750cc802831..00000000000000 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ /dev/null @@ -1,105 +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 { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, -} from 'kibana/public'; - -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { TelemetryPluginStart } from 'src/plugins/telemetry/public'; -import { setServices } from './kibana_services'; -import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; -import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { - Environment, - HomePublicPluginStart, - HomePublicPluginSetup, -} from '../../../../../plugins/home/public'; - -export interface HomePluginStartDependencies { - data: DataPublicPluginStart; - home: HomePublicPluginStart; - telemetry?: TelemetryPluginStart; -} - -export interface HomePluginSetupDependencies { - usageCollection: UsageCollectionSetup; - kibanaLegacy: KibanaLegacySetup; - home: HomePublicPluginSetup; -} - -export class HomePlugin implements Plugin { - private dataStart: DataPublicPluginStart | null = null; - private savedObjectsClient: any = null; - private environment: Environment | null = null; - private featureCatalogue: HomePublicPluginStart['featureCatalogue'] | null = null; - private telemetry?: TelemetryPluginStart; - - constructor(private initializerContext: PluginInitializerContext) {} - - setup( - core: CoreSetup, - { home, kibanaLegacy, usageCollection }: HomePluginSetupDependencies - ) { - kibanaLegacy.registerLegacyApp({ - id: 'home', - title: 'Home', - mount: async (params: AppMountParameters) => { - const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); - const [coreStart, { home: homeStart }] = await core.getStartServices(); - setServices({ - trackUiMetric, - kibanaVersion: this.initializerContext.env.packageInfo.version, - http: coreStart.http, - toastNotifications: core.notifications.toasts, - banners: coreStart.overlays.banners, - docLinks: coreStart.docLinks, - savedObjectsClient: this.savedObjectsClient!, - chrome: coreStart.chrome, - telemetry: this.telemetry, - uiSettings: core.uiSettings, - addBasePath: core.http.basePath.prepend, - getBasePath: core.http.basePath.get, - indexPatternService: this.dataStart!.indexPatterns, - environment: this.environment!, - config: kibanaLegacy.config, - homeConfig: home.config, - tutorialVariables: homeStart.tutorials.get, - featureCatalogue: this.featureCatalogue!, - }); - const { renderApp } = await import('./np_ready/application'); - return await renderApp(params.element); - }, - }); - } - - start(core: CoreStart, { data, home, telemetry }: HomePluginStartDependencies) { - this.environment = home.environment.get(); - this.featureCatalogue = home.featureCatalogue; - this.dataStart = data; - this.telemetry = telemetry; - this.savedObjectsClient = core.savedObjects.client; - } - - stop() {} -} diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/redisenterprise_metrics/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/redisenterprise_metrics/screenshot.png new file mode 100644 index 00000000000000..cc6ef0ce509ebb Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/redisenterprise_metrics/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index 324458c0814d9f..547f44652cf2b1 100644 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ b/src/legacy/core_plugins/kibana/public/index.scss @@ -10,21 +10,18 @@ // vis_type_vislib UI styles @import 'src/legacy/core_plugins/vis_type_vislib/public/index'; -// Dev tools styles -@import './dev_tools/index'; - // Discover styles @import 'discover/index'; -// Home styles -@import './home/index'; - // Visualize styles @import './visualize/index'; // Has to come after visualize because of some // bad cascading in the Editor layout @import 'src/legacy/ui/public/vis/index'; +// Home styles +@import '../../../../plugins/home/public/application/index'; + // Management styles @import './management/index'; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index a83d1176a71977..04eaf2cbe26790 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -26,8 +26,6 @@ import { npSetup } from 'ui/new_platform'; // import the uiExports that we want to "use" import 'uiExports/home'; -import 'uiExports/visTypes'; - import 'uiExports/visualize'; import 'uiExports/savedObjectTypes'; import 'uiExports/fieldFormatEditors'; @@ -44,7 +42,6 @@ import 'uiExports/shareContextMenuExtensions'; import 'uiExports/interpreter'; import 'ui/autoload/all'; -import './home'; import './discover/legacy'; import './visualize/legacy'; import './dashboard/legacy'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss index 2f48ecc322fea5..3a542cacc44be2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss @@ -22,10 +22,6 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ } } -.visEditor__linkedMessage { - padding: $euiSizeS; -} - .visEditor__content { @include flex-parent(); width: 100%; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 4979d9dc89a0ce..9dbb05ea95b487 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -1,28 +1,4 @@ - -