diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky new file mode 100644 index 0000000000000..3f77243da2c1b --- /dev/null +++ b/.ci/Jenkinsfile_flaky @@ -0,0 +1,113 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +// Looks like 'oss:ciGroup:1' or 'oss:firefoxSmoke' +def JOB_PARTS = params.CI_GROUP.split(':') +def IS_XPACK = JOB_PARTS[0] == 'xpack' +def JOB = JOB_PARTS[1] +def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' + +def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) + +def workerFailures = [] + +currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) +currentBuild.description = "${params.CI_GROUP}
Executions: ${params.NUMBER_EXECUTIONS}" + +// Note: If you increase agent count, it will execute NUMBER_EXECUTIONS per agent. It will not divide them up amongst the agents +// e.g. NUMBER_EXECUTIONS = 25, agentCount = 4 results in 100 total executions +def agentCount = 1 + +stage("Kibana Pipeline") { + timeout(time: 180, unit: 'MINUTES') { + timestamps { + ansiColor('xterm') { + def agents = [:] + for(def agentNumber = 1; agentNumber <= agentCount; agentNumber++) { + agents["agent-${agentNumber}"] = { + catchError { + kibanaPipeline.withWorkers('flaky-test-runner', { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld "./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh" + } + } else { + kibanaPipeline.buildXpack() + } + }, getWorkerMap(agentNumber, params.NUMBER_EXECUTIONS.toInteger(), worker, workerFailures))() + } + } + } + + parallel(agents) + + 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" + } + } + } + } +} + +def getWorkerFromParams(isXpack, job, ciGroup) { + if (!isXpack) { + if (job == 'firefoxSmoke') { + return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }) + } else if(job == 'visualRegression') { + return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }) + } else { + return kibanaPipeline.getOssCiGroupWorker(ciGroup) + } + } + + if (job == 'firefoxSmoke') { + return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }) + } else if(job == 'visualRegression') { + return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }) + } else { + return kibanaPipeline.getXpackCiGroupWorker(ciGroup) + } +} + +def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWorkers = 14) { + def workerMap = [:] + def numberOfWorkers = Math.min(numberOfExecutions, maxWorkers) + + for(def i = 1; i <= numberOfWorkers; i++) { + def workerExecutions = numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0) + + workerMap["agent-${agentNumber}-worker-${i}"] = { workerNumber -> + for(def j = 0; j < workerExecutions; j++) { + print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" + withEnv(["JOB=agent-${agentNumber}-worker-${workerNumber}-${j}"]) { + catchError { + try { + worker(workerNumber) + } catch (ex) { + workerFailures << "agent-${agentNumber} worker-${workerNumber}-${j}" + throw ex + } + } + } + } + } + } + + return workerMap +} + +def trunc(str, length) { + if (str.size() >= length) { + return str.take(length) + "..." + } + + return str; +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4ae7b27a58ee4..7001e32754abb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,7 @@ /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/navigation/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui diff --git a/.github/labeler.yml b/.github/labeler.yml index 8a776a909cd9e..57b299912ae9e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -2,6 +2,7 @@ - src/plugins/data/**/* - src/plugins/embeddable/**/* - src/plugins/kibana_react/**/* +- src/plugins/navigation/**/* - src/plugins/kibana_utils/**/* - src/legacy/core_plugins/dashboard_embeddable_container/**/* - src/legacy/core_plugins/data/**/* diff --git a/.i18nrc.json b/.i18nrc.json index 0ea51fe8212c4..d77293e3dc8f7 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -4,6 +4,7 @@ "data": ["src/legacy/core_plugins/data", "src/plugins/data"], "expressions": "src/legacy/core_plugins/expressions", "kibana_react": "src/legacy/core_plugins/kibana_react", + "navigation": "src/legacy/core_plugins/navigation", "server": "src/legacy/server", "console": "src/legacy/core_plugins/console", "core": "src/core", diff --git a/Jenkinsfile b/Jenkinsfile index fca814b265295..4820de9876737 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,7 @@ #!/bin/groovy library 'kibana-pipeline-library' +kibanaLibrary.load() stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit timeout(time: 180, unit: 'MINUTES') { @@ -8,301 +9,42 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a ansiColor('xterm') { catchError { parallel([ - 'kibana-intake-agent': legacyJobRunner('kibana-intake'), - 'x-pack-intake-agent': legacyJobRunner('x-pack-intake'), - 'kibana-oss-agent': withWorkers('kibana-oss-tests', { buildOss() }, [ - 'oss-ciGroup1': getOssCiGroupWorker(1), - 'oss-ciGroup2': getOssCiGroupWorker(2), - 'oss-ciGroup3': getOssCiGroupWorker(3), - 'oss-ciGroup4': getOssCiGroupWorker(4), - 'oss-ciGroup5': getOssCiGroupWorker(5), - 'oss-ciGroup6': getOssCiGroupWorker(6), - 'oss-ciGroup7': getOssCiGroupWorker(7), - 'oss-ciGroup8': getOssCiGroupWorker(8), - 'oss-ciGroup9': getOssCiGroupWorker(9), - 'oss-ciGroup10': getOssCiGroupWorker(10), - 'oss-ciGroup11': getOssCiGroupWorker(11), - 'oss-ciGroup12': getOssCiGroupWorker(12), - 'oss-firefoxSmoke': getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }), - // 'oss-visualRegression': getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }), + 'kibana-intake-agent': kibanaPipeline.legacyJobRunner('kibana-intake'), + 'x-pack-intake-agent': kibanaPipeline.legacyJobRunner('x-pack-intake'), + 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), + 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), + 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), + 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), + 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), + 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), + 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), + 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), + 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), + 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), + 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), + 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), + 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }), + // 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }), ]), - 'kibana-xpack-agent': withWorkers('kibana-xpack-tests', { buildXpack() }, [ - 'xpack-ciGroup1': getXpackCiGroupWorker(1), - 'xpack-ciGroup2': getXpackCiGroupWorker(2), - 'xpack-ciGroup3': getXpackCiGroupWorker(3), - 'xpack-ciGroup4': getXpackCiGroupWorker(4), - 'xpack-ciGroup5': getXpackCiGroupWorker(5), - 'xpack-ciGroup6': getXpackCiGroupWorker(6), - 'xpack-ciGroup7': getXpackCiGroupWorker(7), - 'xpack-ciGroup8': getXpackCiGroupWorker(8), - 'xpack-ciGroup9': getXpackCiGroupWorker(9), - 'xpack-ciGroup10': getXpackCiGroupWorker(10), - 'xpack-firefoxSmoke': getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }), - // 'xpack-visualRegression': getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }), + '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), + 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }), + // 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }), ]), ]) } - node('flyweight') { - // If the build doesn't have a result set by this point, there haven't been any errors and it can be marked as a success - // The e-mail plugin for the infra e-mail depends upon this being set - currentBuild.result = currentBuild.result ?: 'SUCCESS' - - sendMail() - } + kibanaPipeline.sendMail() } } } } - -def withWorkers(name, preWorkerClosure = {}, workerClosures = [:]) { - return { - jobRunner('tests-xl', true) { - try { - doSetup() - preWorkerClosure() - - def nextWorker = 1 - def worker = { workerClosure -> - def workerNumber = nextWorker - nextWorker++ - - return { - workerClosure(workerNumber) - } - } - - def workers = [:] - workerClosures.each { workerName, workerClosure -> - workers[workerName] = worker(workerClosure) - } - - parallel(workers) - } finally { - catchError { - uploadAllGcsArtifacts(name) - } - - catchError { - runbldJunit() - } - - catchError { - publishJunit() - } - - catchError { - runErrorReporter() - } - } - } - } -} - -def getPostBuildWorker(name, closure) { - return { workerNumber -> - def kibanaPort = "61${workerNumber}1" - def esPort = "61${workerNumber}2" - def esTransportPort = "61${workerNumber}3" - - withEnv([ - "CI_WORKER_NUMBER=${workerNumber}", - "TEST_KIBANA_HOST=localhost", - "TEST_KIBANA_PORT=${kibanaPort}", - "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", - "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", - "TEST_ES_TRANSPORT_PORT=${esTransportPort}", - "IS_PIPELINE_JOB=1", - ]) { - closure() - } - } -} - -def getOssCiGroupWorker(ciGroup) { - return getPostBuildWorker("ciGroup" + ciGroup, { - withEnv([ - "CI_GROUP=${ciGroup}", - "JOB=kibana-ciGroup${ciGroup}", - ]) { - runbld "./test/scripts/jenkins_ci_group.sh" - } - }) -} - -def getXpackCiGroupWorker(ciGroup) { - return getPostBuildWorker("xpack-ciGroup" + ciGroup, { - withEnv([ - "CI_GROUP=${ciGroup}", - "JOB=xpack-kibana-ciGroup${ciGroup}", - ]) { - runbld "./test/scripts/jenkins_xpack_ci_group.sh" - } - }) -} - -def legacyJobRunner(name) { - return { - parallel([ - "${name}": { - withEnv([ - "JOB=${name}", - ]) { - jobRunner('linux && immutable', false) { - try { - runbld('.ci/run.sh', true) - } finally { - catchError { - uploadAllGcsArtifacts(name) - } - catchError { - publishJunit() - } - catchError { - runErrorReporter() - } - } - } - } - } - ]) - } -} - -def jobRunner(label, useRamDisk, closure) { - node(label) { - if (useRamDisk) { - // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm - def originalWorkspace = env.WORKSPACE - ws('/tmp/workspace') { - sh """ - mkdir -p /dev/shm/workspace - mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist - rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it - ln -s /dev/shm/workspace '${originalWorkspace}' - """ - } - } - - def scmVars = checkout scm - - withEnv([ - "CI=true", - "HOME=${env.JENKINS_HOME}", - "PR_SOURCE_BRANCH=${env.ghprbSourceBranch ?: ''}", - "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", - "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", - "TEST_BROWSER_HEADLESS=1", - "GIT_BRANCH=${scmVars.GIT_BRANCH}", - ]) { - withCredentials([ - string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), - string(credentialsId: 'vault-role-id', variable: 'VAULT_ROLE_ID'), - string(credentialsId: 'vault-secret-id', variable: 'VAULT_SECRET_ID'), - ]) { - // scm is configured to check out to the ./kibana directory - dir('kibana') { - closure() - } - } - } - } -} - -// TODO what should happen if GCS, Junit, or email publishing fails? Unstable build? Failed build? - -def uploadGcsArtifact(workerName, pattern) { - def storageLocation = "gs://kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" // TODO - // def storageLocation = "gs://kibana-pipeline-testing/jobs/pipeline-test/${BUILD_NUMBER}/${workerName}" - - googleStorageUpload( - credentialsId: 'kibana-ci-gcs-plugin', - bucket: storageLocation, - pattern: pattern, - sharedPublicly: true, - showInline: true, - ) -} - -def uploadAllGcsArtifacts(workerName) { - def ARTIFACT_PATTERNS = [ - 'target/kibana-*', - 'target/junit/**/*', - 'test/**/screenshots/**/*.png', - 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/**/*.png', - 'x-pack/test/functional/failure_debug/html/*.html', - 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', - ] - - ARTIFACT_PATTERNS.each { pattern -> - uploadGcsArtifact(workerName, pattern) - } -} - -def publishJunit() { - junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) -} - -def sendMail() { - sendInfraMail() - sendKibanaMail() -} - -def sendInfraMail() { - catchError { - step([ - $class: 'Mailer', - notifyEveryUnstableBuild: true, - recipients: 'infra-root+build@elastic.co', - sendToIndividuals: false - ]) - } -} - -def sendKibanaMail() { - catchError { - def buildStatus = buildUtils.getBuildStatus() - - if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { - emailext( - to: 'build-kibana@elastic.co', - subject: "${env.JOB_NAME} - Build # ${env.BUILD_NUMBER} - ${buildStatus}", - body: '${SCRIPT,template="groovy-html.template"}', - mimeType: 'text/html', - ) - } - } -} - -def runbld(script, enableJunitProcessing = false) { - def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" - - sh "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}" -} - -def runbldJunit() { - sh "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh" -} - -def bash(script) { - sh "#!/bin/bash\n${script}" -} - -def doSetup() { - runbld "./test/scripts/jenkins_setup.sh" -} - -def buildOss() { - runbld "./test/scripts/jenkins_build_kibana.sh" -} - -def buildXpack() { - runbld "./test/scripts/jenkins_xpack_build_kibana.sh" -} - -def runErrorReporter() { - bash """ - source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests - """ -} diff --git a/config/kibana.yml b/config/kibana.yml index 9525a6423d90a..47482f9e6d59f 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -50,7 +50,8 @@ #server.ssl.key: /path/to/your/server.key # Optional settings that provide the paths to the PEM-format SSL certificate and key files. -# These files validate that your Elasticsearch backend uses the same key files. +# These files are used to verify the identity of Kibana to Elasticsearch and are required when +# xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. #elasticsearch.ssl.certificate: /path/to/your/client.crt #elasticsearch.ssl.key: /path/to/your/client.key diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index df4b17e905772..07f3cf028dc0e 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -25,6 +25,32 @@ A † denotes an argument can be passed multiple times. Returns `true` if all of the conditions are met. See also <>. +*Expression syntax* +[source,js] +---- +all {neq “foo”} {neq “bar”} {neq “fizz”} +all condition={gt 10} condition={lt 20} +---- + +*Code example* +[source,text] +---- +filters +| demodata +| math "mean(percent_uptime)" +| formatnumber "0.0%" +| metric "Average uptime" + metricFont={ + font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" + color={ + if {all {gte 0} {lt 0.8}} then="red" else="green" + } + align="center" lHeight=48 + } +| render +---- +This sets the color of the metric text to `”red”` if the context passed into `metric` is greater than or equal to 0 and less than 0.8. Otherwise, the color is set to `"green"`. + *Accepts:* `null` [cols="3*^<"] @@ -47,6 +73,24 @@ Alias: `condition` Converts between core types, including `string`, `number`, `null`, `boolean`, and `date`, and renames columns. See also <> and <>. +*Expression syntax* +[source,js] +---- +alterColumn “cost” type=”string” +alterColumn column=”@timestamp” name=”foo” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| alterColumn “time” name=”time_in_ms” type=”number” +| table +| render +---- +This renames the `time` column to `time_in_ms` and converts the type of the column’s values from `date` to `number`. + *Accepts:* `datatable` [cols="3*^<"] @@ -77,6 +121,27 @@ Alias: `column` Returns `true` if at least one of the conditions is met. See also <>. +*Expression syntax* +[source,js] +---- +any {eq “foo”} {eq “bar”} {eq “fizz”} +any condition={lte 10} condition={gt 30} +---- + +*Code example* +[source,text] +---- +filters +| demodata +| filterrows { + getCell "project" | any {eq "elasticsearch"} {eq "kibana"} {eq "x-pack"} + } +| pointseries color="project" size="max(price)" +| pie +| render +---- +This filters out any rows that don’t contain `“elasticsearch”`, `“kibana”` or `“x-pack”` in the `project` field. + *Accepts:* `null` [cols="3*^<"] @@ -99,6 +164,28 @@ Alias: `condition` Creates a `datatable` with a single value. See also <>. +*Expression syntax* +[source,js] +---- +as +as “foo” +as name=”bar” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| ply by="project" fn={math "count(username)" | as "num_users"} fn={math "mean(price)" | as "price"} +| pointseries x="project" y="num_users" size="price" color="project" +| plot +| render +---- +`as` casts any primitive value (`string`, `number`, `date`, `null`) into a `datatable` with a single row and a single column with the given name (or defaults to `"value"` if no name is provided). This is useful when piping a primitive value into a function that only takes `datatable` as an input. + +In the example above, `ply` expects each `fn` subexpression to return a `datatable` in order to merge the results of each `fn` back into a `datatable`, but using a `math` aggregation in the subexpressions returns a single `math` value, which is then cast into a `datatable` using `as`. + *Accepts:* `string`, `boolean`, `number`, `null` [cols="3*^<"] @@ -123,6 +210,21 @@ Default: `"value"` Retrieves Canvas workpad asset objects to provide as argument values. Usually images. +*Expression syntax* +[source,js] +---- +asset "asset-52f14f2b-fee6-4072-92e8-cd2642665d02" +asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114" +---- + +*Code example* +[source,text] +---- +image dataurl={asset "asset-c661a7cc-11be-45a1-a401-d7592ea7917a"} mode="contain" +| render +---- +The image asset stored with the ID `“asset-c661a7cc-11be-45a1-a401-d7592ea7917a”` is passed into the `dataurl` argument of the `image` function to display the stored asset. + *Accepts:* `null` [cols="3*^<"] @@ -145,6 +247,27 @@ Alias: `id` Configures the axis of a visualization. Only used with <>. +*Expression syntax* +[source,js] +---- +axisConfig show=false +axisConfig position=”right” min=0 max=10 tickSize=1 +---- + +*Code example* +[source,text] +---- +filters +| demodata +| pointseries x="size(cost)" y="project" color="project" +| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} + legend=false + xaxis={axisConfig position="top" min=0 max=400 tickSize=100} + yaxis={axisConfig position="right"} +| render +---- +This sets the `x-axis` to display on the top of the chart and sets the range of values to `0-400` with ticks displayed at `100` intervals. The `y-axis` is configured to display on the `right`. + *Accepts:* `null` [cols="3*^<"] @@ -188,6 +311,34 @@ Default: `true` Builds a `case` (including a condition/result) to pass to the <> function. +*Expression syntax* +[source,js] +---- +case 0 then=”red” +case when=5 then=”yellow” +case if={lte 50} then=”green” +---- + +*Code example* +[source,text] +---- +math "random()" +| progress shape="gauge" label={formatnumber "0%"} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" + color={ + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} + default="red" + }} + valueColor={ + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} + default="red" + } +| render +---- +This sets the color of the progress indicator and the color of the label to `"green"` if the value is less than or equal to `0.5`, `"orange"` if the value is greater than `0.5` and less than or equal to `0.75`, and `"red"` if `none` of the case conditions are met. + *Accepts:* `any` [cols="3*^<"] @@ -229,6 +380,24 @@ Clears the _context_, and returns `null`. Includes or excludes columns from a data table. If you specify both, this will exclude first. +*Expression syntax* +[source,js] +---- +columns include=”@timestamp, projects, cost” +columns exclude=”username, country, age” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| columns include="price, cost, state, project" +| table +| render +---- +This only keeps the `price`, `cost`, `state`, and `project` columns from the `demodata` data source and removes all other columns. + *Accepts:* `datatable` [cols="3*^<"] @@ -257,6 +426,31 @@ Compares the _context_ to specified value to determine `true` or `false`. Usually used in combination with <> or <>. This only works with primitive types, such as `number`, `string`, and `boolean`. See also <>, <>, <>, <>, <>, and <>. +*Expression syntax* +[source,js] +---- +compare “neq” to=”elasticsearch” +compare op=”lte” to=100 +---- + +*Code example* +[source,text] +---- +filters +| demodata +| mapColumn project + fn=${getCell project | + switch + {case if={compare eq to=kibana} then=kibana} + {case if={compare eq to=elasticsearch} then=elasticsearch} + default="other" + } +| pointseries size="size(cost)" color="project" +| pie +| render +---- +This maps all `project` values that aren’t `“kibana”` and `“elasticsearch”` to `“other”`. Alternatively, you can use the individual comparator functions instead of compare. See <>, <>, <>, <>, <>, and <>. + *Accepts:* `string`, `number`, `boolean`, `null` [cols="3*^<"] @@ -287,6 +481,35 @@ Alias: `this`, `b` Creates an object used for styling an element's container, including background, border, and opacity. +*Expression syntax* +[source,js] +---- +containerStyle backgroundColor=”red”’ +containerStyle borderRadius=”50px” +containerStyle border=”1px solid black” +containerStyle padding=”5px” +containerStyle opacity=”0.5” +containerStyle overflow=”hidden” +containerStyle backgroundImage={asset id=asset-f40d2292-cf9e-4f2c-8c6f-a504a25e949c} + backgroundRepeat="no-repeat" + backgroundSize="cover" +---- + +*Code example* +[source,text] +---- +shape "star" fill="#E61D35" maintainAspect=true +| render + containerStyle={ + containerStyle backgroundColor="#F8D546" + borderRadius="200px" + border="4px solid #05509F" + padding="0px" + opacity="0.9" + overflow="hidden" + } +---- + *Accepts:* `null` [cols="3*^<"] @@ -346,6 +569,22 @@ Default: `"hidden"` Returns whatever you pass into it. This can be useful when you need to use the _context_ as an argument to a function as a sub-expression. +*Expression syntax* +[source,js] +---- +context +---- + +*Code example* +[source,text] +---- +date +| formatdate "LLLL" +| markdown "Last updated: " {context} +| render +---- +Using the `context` function allows us to pass the output, or _context_, of the previous function as a value to an argument in the next function. Here we get the formatted date string from the previous function and pass it as `content` for the markdown element. + *Accepts:* `any` *Returns:* Original _context_ @@ -356,6 +595,26 @@ _context_ as an argument to a function as a sub-expression. Creates a `datatable` from CSV input. +*Expression syntax* +[source,js] +---- +csv “fruit, stock + kiwi, 10 + Banana, 5” +---- + +*Code example* +[source,text] +---- +csv "fruit,stock + kiwi,10 + banana,5" +| pointseries color=fruit size=stock +| pie +| render +---- +This is useful for quickly mocking data. + *Accepts:* `null` [cols="3*^<"] @@ -390,6 +649,30 @@ Alias: `data` Returns the current time, or a time parsed from a `string`, as milliseconds since epoch. +*Expression syntax* +[source,js] +---- +date +date value=1558735195 +date “2019-05-24T21:59:55+0000” +date “01/31/2019” format=”MM/DD/YYYY” +---- + +*Code example* +[source,text] +---- +date +| formatdate "LLL" +| markdown {context} + font={font family="Arial, sans-serif" size=30 align="left" + color="#000000" + weight="normal" + underline=false + italic=false} +| render +---- +Using `date` without passing any arguments will return the current date and time. + *Accepts:* `null` [cols="3*^<"] @@ -418,6 +701,24 @@ using the `format` argument. Must be an ISO8601 string or you must provide the f A mock data set that includes project CI times with usernames, countries, and run phases. +*Expression syntax* +[source,js] +---- +demodata +demodata “ci” +demodata type=”shirts” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| table +| render +---- +`demodata` is a mock data set that you can use to start playing around in Canvas. + *Accepts:* `filter` [cols="3*^<"] @@ -442,6 +743,23 @@ Default: `"ci"` Executes multiple sub-expressions, then returns the original _context_. Use for running functions that produce an action or side effect without changing the original _context_. +*Expression syntax* +[source,js] +---- +do fn={something cool} +---- + +*Code example* +[source,text] +---- +filters +| demodata +| do fn={something cool} +| table +| render +---- +`do` should be used to invoke a function that produces as a side effect without changing the `context`. + *Accepts:* `any` [cols="3*^<"] @@ -464,6 +782,22 @@ Aliases: `expression`, `exp`, `fn`, `function` Configures a dropdown filter control element. +*Expression syntax* +[source,js] +---- +dropdownControl valueColumn=project filterColumn=project +dropdownControl valueColumn=agent filterColumn=agent.keyword filterGroup=group1 +---- + +*Code example* +[source,text] +---- +demodata +| dropdownControl valueColumn=project filterColumn=project +| render +---- +This creates a dropdown filter element. It requires a data source and uses the unique values from the given `valueColumn` (i.e. `project`) and applies the filter to the `project` column. Note: `filterColumn` should point to a keyword type field for Elasticsearch data sources. + *Accepts:* `datatable` [cols="3*^<"] @@ -496,6 +830,33 @@ Configures a dropdown filter control element. Returns whether the _context_ is equal to the argument. +*Expression syntax* +[source,js] +---- +eq true +eq null +eq 10 +eq “foo” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| mapColumn project + fn=${getCell project | + switch + {case if={eq kibana} then=kibana} + {case if={eq elasticsearch} then=elasticsearch} + default="other" + } +| pointseries size="size(cost)" color="project" +| pie +| render +---- +This changes all values in the project column that don’t equal `“kibana”` or `“elasticsearch”` to `“other”`. + *Accepts:* `boolean`, `number`, `string`, `null` [cols="3*^<"] @@ -518,6 +879,28 @@ Alias: `value` Queries {es} for the number of hits matching the specified query. +*Expression syntax* +[source,js] +---- +escount index=”logstash-*” +escount "currency:\"EUR\"" index=”kibana_sample_data_ecommerce” +escount query="response:404" index=”kibana_sample_data_logs” +---- + +*Code example* +[source,text] +---- +filters +| escount "Cancelled:true" index="kibana_sample_data_flights" +| math "value" +| progress shape="semicircle" + label={formatnumber 0,0} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center} + max={filters | escount index="kibana_sample_data_flights"} +| render +---- +The first `escount` expression retrieves the number of flights that were cancelled. The second `escount` expression retrieves the total number of flights. + *Accepts:* `filter` [cols="3*^<"] @@ -549,6 +932,34 @@ Default: `_all` Queries {es} for raw documents. Specify the fields you want to retrieve, especially if you are asking for a lot of rows. +*Expression syntax* +[source,js] +---- +esdocs index=”logstash-*” +esdocs "currency:\"EUR\"" index=”kibana_sample_data_ecommerce” +esdocs query="response:404" index=”kibana_sample_data_logs” +esdocs index=”kibana_sample_data_flights” count=100 +esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc" +---- + +*Code example* +[source,text] +---- +filters +| esdocs index="kibana_sample_data_ecommerce" + fields="customer_gender, taxful_total_price, order_date" + sort="order_date, asc" + count=10000 +| mapColumn "order_date" + fn={getCell "order_date" | date {context} | rounddate "YYYY-MM-DD"} +| alterColumn "order_date" type="date" +| pointseries x="order_date" y="sum(taxful_total_price)" color="customer_gender" +| plot defaultStyle={seriesStyle lines=3} + palette={palette "#7ECAE3" "#003A4D" gradient=true} +| render +---- +This retrieves the latest 10000 documents data from the `kibana_sample_data_ecommerce` index sorted by `order_date` in ascending order and only requests the `customer_gender`, `taxful_total_price`, and `order_date` fields. + *Accepts:* `filter` [cols="3*^<"] @@ -593,6 +1004,21 @@ Default: `"_all"` Queries {es} using {es} SQL. +*Expression syntax* +[source,js] +---- +essql query=”SELECT * FROM \”logstash*\”” +essql “SELECT * FROM \”apm*\”” count=10000 +---- + +*Code example* +[source,text] +---- +filters +| essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" +---- +This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from the “kibana_sample_data_flights” index. + *Accepts:* `filter` [cols="3*^<"] @@ -627,6 +1053,26 @@ Default: `UTC` Creates a filter that matches a given column to an exact value. +*Expression syntax* +[source,js] +---- +exactly “state” value=”running” +exactly “age” value=50 filterGroup=”group2” +exactly column=“project” value=”beats” +---- + +*Code example* +[source,text] +---- +filters +| exactly column=project value=elasticsearch +| demodata +| pointseries x=project y="mean(age)" +| plot defaultStyle={seriesStyle bars=1} +| render +---- +The `exactly` filter here is added to existing filters retrieved by the `filters` function and further filters down the data to only have `”elasticsearch”` data. The `exactly` filter only applies to this one specific element and will not affect other elements in the workpad. + *Accepts:* `filter` [cols="3*^<"] @@ -664,6 +1110,29 @@ capitalization. Filters rows in a `datatable` based on the return value of a sub-expression. +*Expression syntax* +[source,js] +---- +filterrows {getCell “project” | eq “kibana”} +filterrows fn={getCell “age” | gt 50} +---- + +*Code example* +[source,text] +---- +filters +| demodata +| filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} +| mapColumn "@timestamp" + fn={getCell "@timestamp" | rounddate "YYYY-MM"} +| alterColumn "@timestamp" type="date" +| pointseries x="@timestamp" y="mean(cost)" color="country" +| plot defaultStyle={seriesStyle points="2" lines="1"} + palette={palette "#01A4A4" "#CC6666" "#D0D102" "#616161" "#00A1CB" "#32742C" "#F18D05" "#113F8C" "#61AE24" "#D70060" gradient=false} +| render +---- +This uses `filterrows` to only keep data from India (`IN`), the United States (`US`), and China (`CN`). + *Accepts:* `datatable` [cols="3*^<"] @@ -688,6 +1157,34 @@ and a `false` value removes it. Aggregates element filters from the workpad for use elsewhere, usually a data source. +*Expression syntax* +[source,js] +---- +filters +filters group=”timefilter1” +filters group=”timefilter2” group=”dropdownfilter1” ungrouped=true +---- + +*Code example* +[source,text] +---- +filters group=group2 ungrouped=true +| demodata +| pointseries x="project" y="size(cost)" color="project" +| plot defaultStyle={seriesStyle bars=0.75} legend=false + font={ + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true + italic=true + } +| render +---- +`filters` sets the existing filters as context and accepts `group` parameter to create filter groups. + *Accepts:* `null` [cols="3*^<"] @@ -718,6 +1215,38 @@ Default: `false` Creates a font style. +*Expression syntax* +[source,js] +---- +font size=12 +font family=Arial +font align=middle +font color=pink +font weight=lighter +font underline=true +font italic=false +font lHeight=32 +---- + +*Code example* +[source,text] +---- +filters +| demodata +| pointseries x="project" y="size(cost)" color="project" +| plot defaultStyle={seriesStyle bars=0.75} legend=false + font={ + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true + italic=true + } +| render +---- + *Accepts:* `null` [cols="3*^<"] @@ -781,6 +1310,25 @@ Default: `"normal"` Formats an ISO8601 date string or a date in milliseconds since epoch using MomentJS. See https://momentjs.com/docs/#/displaying/. +*Expression syntax* +[source,js] +---- +formatdate format=”YYYY-MM-DD” +formatdate “MM/DD/YYYY” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| mapColumn "time" fn={getCell time | formatdate "MMM 'YY"} +| pointseries x="time" y="sum(price)" color="state" +| plot defaultStyle={seriesStyle points=5} +| render +---- +This transforms the dates in the `time` field into strings that look like `“Jan ‘19”`, `“Feb ‘19”`, etc. using a MomentJS format. + *Accepts:* `number`, `string` [cols="3*^<"] @@ -803,6 +1351,26 @@ Alias: `format` Formats a `number` into a formatted `string` using NumeralJS. See http://numeraljs.com/#format. +*Expression syntax* +[source,js] +---- +formatnumber format=”$0,0.00” +formatnumber “0.0a” +---- + +*Code example* +[source,text] +---- +filters +| demodata +| math "mean(percent_uptime)" +| progress shape="gauge" + label={formatnumber "0%"} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align="center"} +| render +---- +The `formatnumber` subexpression receives the same `context` as the `progress` function, which is the output of the `math` function. It formats the value into a percentage. + *Accepts:* `number` [cols="3*^<"] diff --git a/docs/development/core/public/kibana-plugin-public.chromedoctitle.change.md b/docs/development/core/public/kibana-plugin-public.chromedoctitle.change.md new file mode 100644 index 0000000000000..eba149bf93a4c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromedoctitle.change.md @@ -0,0 +1,34 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) > [change](./kibana-plugin-public.chromedoctitle.change.md) + +## ChromeDocTitle.change() method + +Changes the current document title. + +Signature: + +```typescript +change(newTitle: string | string[]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newTitle | string | string[] | | + +Returns: + +`void` + +## Example + +How to change the title of the document + +```ts +chrome.docTitle.change('My application title') +chrome.docTitle.change(['My application', 'My section']) + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.chromedoctitle.md b/docs/development/core/public/kibana-plugin-public.chromedoctitle.md new file mode 100644 index 0000000000000..3c6cfab486288 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromedoctitle.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) + +## ChromeDocTitle interface + +APIs for accessing and updating the document title. + +Signature: + +```typescript +export interface ChromeDocTitle +``` + +## Methods + +| Method | Description | +| --- | --- | +| [change(newTitle)](./kibana-plugin-public.chromedoctitle.change.md) | Changes the current document title. | +| [reset()](./kibana-plugin-public.chromedoctitle.reset.md) | Resets the document title to it's initial value. (meaning the one present in the title meta at application load.) | + +## Example 1 + +How to change the title of the document + +```ts +chrome.docTitle.change('My application') + +``` + +## Example 2 + +How to reset the title of the document to it's initial value + +```ts +chrome.docTitle.reset() + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.chromedoctitle.reset.md b/docs/development/core/public/kibana-plugin-public.chromedoctitle.reset.md new file mode 100644 index 0000000000000..4b4c6f573e006 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromedoctitle.reset.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) > [reset](./kibana-plugin-public.chromedoctitle.reset.md) + +## ChromeDocTitle.reset() method + +Resets the document title to it's initial value. (meaning the one present in the title meta at application load.) + +Signature: + +```typescript +reset(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.chromestart.doctitle.md b/docs/development/core/public/kibana-plugin-public.chromestart.doctitle.md new file mode 100644 index 0000000000000..71eda64c24646 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromestart.doctitle.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeStart](./kibana-plugin-public.chromestart.md) > [docTitle](./kibana-plugin-public.chromestart.doctitle.md) + +## ChromeStart.docTitle property + +APIs for accessing and updating the document title. + +Signature: + +```typescript +docTitle: ChromeDocTitle; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromestart.md b/docs/development/core/public/kibana-plugin-public.chromestart.md index bc41aa10cce8f..153e06d591404 100644 --- a/docs/development/core/public/kibana-plugin-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-public.chromestart.md @@ -16,6 +16,7 @@ export interface ChromeStart | Property | Type | Description | | --- | --- | --- | +| [docTitle](./kibana-plugin-public.chromestart.doctitle.md) | ChromeDocTitle | APIs for accessing and updating the document title. | | [navControls](./kibana-plugin-public.chromestart.navcontrols.md) | ChromeNavControls | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | | [navLinks](./kibana-plugin-public.chromestart.navlinks.md) | ChromeNavLinks | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. | | [recentlyAccessed](./kibana-plugin-public.chromestart.recentlyaccessed.md) | ChromeRecentlyAccessed | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 57ab8bedde95e..253e50f0f2c2e 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -32,6 +32,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | +| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. | | [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | | | [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | | [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 2f81afacf4bb4..5a8b702e0ec99 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -90,10 +90,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) | | | [SavedObjectsBulkResponse](./kibana-plugin-server.savedobjectsbulkresponse.md) | | | [SavedObjectsBulkUpdateObject](./kibana-plugin-server.savedobjectsbulkupdateobject.md) | | +| [SavedObjectsBulkUpdateOptions](./kibana-plugin-server.savedobjectsbulkupdateoptions.md) | | | [SavedObjectsBulkUpdateResponse](./kibana-plugin-server.savedobjectsbulkupdateresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | +| [SavedObjectsDeleteOptions](./kibana-plugin-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. | | [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | | @@ -149,6 +151,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. | | [MIGRATION\_ASSISTANCE\_INDEX\_ACTION](./kibana-plugin-server.migration_assistance_index_action.md) | | | [MIGRATION\_DEPRECATION\_LEVEL](./kibana-plugin-server.migration_deprecation_level.md) | | +| [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | diff --git a/docs/development/core/server/kibana-plugin-server.mutatingoperationrefreshsetting.md b/docs/development/core/server/kibana-plugin-server.mutatingoperationrefreshsetting.md new file mode 100644 index 0000000000000..94c8fa8c22ef6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.mutatingoperationrefreshsetting.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) + +## MutatingOperationRefreshSetting type + +Elasticsearch Refresh setting for mutating operation + +Signature: + +```typescript +export declare type MutatingOperationRefreshSetting = boolean | 'wait_for'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateoptions.md new file mode 100644 index 0000000000000..920a6ca224df4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateOptions](./kibana-plugin-server.savedobjectsbulkupdateoptions.md) + +## SavedObjectsBulkUpdateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-server.savedobjectsbulkupdateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateoptions.refresh.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateoptions.refresh.md new file mode 100644 index 0000000000000..35e9e6483da10 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateOptions](./kibana-plugin-server.savedobjectsbulkupdateoptions.md) > [refresh](./kibana-plugin-server.savedobjectsbulkupdateoptions.refresh.md) + +## SavedObjectsBulkUpdateOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md index 107e71959f706..30db524ffa02c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md @@ -9,7 +9,7 @@ Bulk Updates multiple SavedObject at once Signature: ```typescript -bulkUpdate(objects: Array>, options?: SavedObjectsBaseOptions): Promise>; +bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; ``` ## Parameters @@ -17,7 +17,7 @@ bulkUpdate(objects: ArrayArray<SavedObjectsBulkUpdateObject<T>> | | -| options | SavedObjectsBaseOptions | | +| options | SavedObjectsBulkUpdateOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md index 657f56d591e77..c20c7e886490a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md @@ -9,7 +9,7 @@ Deletes a SavedObject Signature: ```typescript -delete(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<{}>; +delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; ``` ## Parameters @@ -18,7 +18,7 @@ delete(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<{}> | --- | --- | --- | | type | string | | | id | string | | -| options | SavedObjectsBaseOptions | | +| options | SavedObjectsDeleteOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md index 16549e420ac01..e4ad636056915 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md @@ -19,4 +19,5 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [overwrite](./kibana-plugin-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | | [references](./kibana-plugin-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | +| [refresh](./kibana-plugin-server.savedobjectscreateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.refresh.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.refresh.md new file mode 100644 index 0000000000000..785874a12c8c4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) > [refresh](./kibana-plugin-server.savedobjectscreateoptions.refresh.md) + +## SavedObjectsCreateOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsdeleteoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsdeleteoptions.md new file mode 100644 index 0000000000000..2c641ba5cc8d8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsdeleteoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsDeleteOptions](./kibana-plugin-server.savedobjectsdeleteoptions.md) + +## SavedObjectsDeleteOptions interface + + +Signature: + +```typescript +export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-server.savedobjectsdeleteoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsdeleteoptions.refresh.md b/docs/development/core/server/kibana-plugin-server.savedobjectsdeleteoptions.refresh.md new file mode 100644 index 0000000000000..782c52956f297 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsdeleteoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsDeleteOptions](./kibana-plugin-server.savedobjectsdeleteoptions.md) > [refresh](./kibana-plugin-server.savedobjectsdeleteoptions.refresh.md) + +## SavedObjectsDeleteOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md index 7fcd362e937a0..49e8946ad2826 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md @@ -16,5 +16,6 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | | [references](./kibana-plugin-server.savedobjectsupdateoptions.references.md) | SavedObjectReference[] | A reference to another saved object. | +| [refresh](./kibana-plugin-server.savedobjectsupdateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | | [version](./kibana-plugin-server.savedobjectsupdateoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.refresh.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.refresh.md new file mode 100644 index 0000000000000..bb1142c242012 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) > [refresh](./kibana-plugin-server.savedobjectsupdateoptions.refresh.md) + +## SavedObjectsUpdateOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index 08f65b0a24091..eafbb7d8f7c91 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -183,7 +183,7 @@ At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] API to load the data sets: [source,shell] -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/bank/account/_bulk?pretty' --data-binary @accounts.json +curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/bank/_bulk?pretty' --data-binary @accounts.json curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/shakespeare/_bulk?pretty' --data-binary @shakespeare.json curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/_bulk?pretty' --data-binary @logs.jsonl diff --git a/docs/infrastructure/getting-started.asciidoc b/docs/infrastructure/getting-started.asciidoc index 151a8e2928cf8..1c5645f5a6e4e 100644 --- a/docs/infrastructure/getting-started.asciidoc +++ b/docs/infrastructure/getting-started.asciidoc @@ -5,7 +5,7 @@ To get started with the Metrics app in Kibana, you need to start collecting metrics data for your infrastructure. Kibana provides step-by-step instructions to help you add metrics data. -The {infra-guide}[Infrastructure Monitoring Guide] is a good source for more detailed information and instructions. +The {metrics-guide}[Metrics Monitoring Guide] is a good source for more detailed information and instructions. [role="screenshot"] image::infrastructure/images/metrics-add-data.png[Screenshot showing Add metric data to Kibana UI] diff --git a/docs/logs/getting-started.asciidoc b/docs/logs/getting-started.asciidoc index 1ed8798a4b87f..ca09bb34c0e56 100644 --- a/docs/logs/getting-started.asciidoc +++ b/docs/logs/getting-started.asciidoc @@ -5,7 +5,7 @@ To get started with the Logs app in Kibana, you need to start collecting logs data for your infrastructure. Kibana provides step-by-step instructions to help you add logs data. -The {infra-guide}[Infrastructure Monitoring Guide] is a good source for more detailed information and instructions. +The {logs-guide}[Logs Monitoring Guide] is a good source for more detailed information and instructions. [role="screenshot"] image::logs/images/logs-add-data.png[Screenshot showing Add logging data in Kibana] diff --git a/docs/management/snapshot-restore/images/create-policy-example.png b/docs/management/snapshot-restore/images/create-policy-example.png new file mode 100755 index 0000000000000..e871c925f5fd5 Binary files /dev/null and b/docs/management/snapshot-restore/images/create-policy-example.png differ diff --git a/docs/management/snapshot-restore/images/snapshot-retention.png b/docs/management/snapshot-restore/images/snapshot-retention.png new file mode 100755 index 0000000000000..7b390357a21b6 Binary files /dev/null and b/docs/management/snapshot-restore/images/snapshot-retention.png differ diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc index f309b2c17e0ed..f19aaa122675e 100644 --- a/docs/management/snapshot-restore/index.asciidoc +++ b/docs/management/snapshot-restore/index.asciidoc @@ -11,11 +11,11 @@ you can restore a snapshot from the repository. You’ll find *Snapshot and Restore* under *Management > Elasticsearch*. With this UI, you can: -* <> -* <> -* <> -* <> -* <> +* Register a repository for storing your snapshots +* View a list of your snapshots and drill down into details +* Restore data into your cluster from a snapshot +* Create a policy to automate snapshot creation and deletion +* Delete a snapshot to free storage space [role="screenshot"] image:management/snapshot-restore/images/snapshot_list.png["Snapshot list"] @@ -27,28 +27,34 @@ more detailed information. [float] [[kib-snapshot-register-repository]] === Register a repository +A repository is where your snapshots live. You must register a snapshot +repository before you can perform snapshot and restore operations. + +If you don't have a repository, Kibana walks you through the process of +registering one. +{kib} supports three repository types +out of the box: shared file system, read-only URL, and source-only. +For more information on these repositories and their settings, +see {ref}/modules-snapshots.html#snapshots-repositories[Repositories]. +To use other repositories, such as S3, see +{ref}/modules-snapshots.html#_repository_plugins[Repository plugins]. -The *Repositories* view provides an overview of your repositories. -Click a repository name to view its type, number of snapshots, and settings, and also to verify status. + +Once you create a repository, it is listed in the *Repositories* +view. +Click a repository name to view its type, number of snapshots, and settings, +and to verify status. [role="screenshot"] image:management/snapshot-restore/images/repository_list.png["Repository list"] -If you don't have a repository, you're prompted to register one. -{es} supports three repository types -out of the box: shared file system, read-only URL, and source-only. -For more information on these repositories and their settings, -see {ref}/modules-snapshots.html#snapshots-repositories[Repositories]. For an example, -see <>. - -To use other repositories, such as S3, you can install plugins. See -{ref}/modules-snapshots.html#_repository_plugins[Repository plugins]. [float] [[kib-view-snapshot]] === View your snapshots -The *Snapshots* view gives an overview of your snapshots. You can drill down +A snapshot is a backup taken from a running {es} cluster. You'll find an overview of +your snapshots in the *Snapshots* view, and you can drill down into each snapshot for further investigation. [role="screenshot"] @@ -68,18 +74,25 @@ the new data. [[kib-restore-snapshot]] === Restore a snapshot -The *Restore* wizard walks you through the process of restoring a snapshot -into a running cluster. To get started, go to the *Snapshots* view, find the -snapshot, and click the restore icon in the *Actions* column. +The information stored in a snapshot is not tied to a specific +cluster or a cluster name. This enables you to +restore a snapshot made from one cluster to another cluster. You might +use the restore operation to: -You’re presented -options for the restore, including which +* Recover data lost due to a failure +* Migrate a current Elasticsearch cluster to a new version +* Move data from one cluster to another cluster + +To get started, go to the *Snapshots* view, find the +snapshot, and click the restore icon in the *Actions* column. +The Restore wizard presents +options for the restore operation, including which indices to restore and whether to modify the index settings. You can restore an existing index only if it’s closed and has the same number of shards as the index in the snapshot. Once you initiate the restore, you're navigated to the *Restore Status* view, -where you can track the progress. +where you can track the current state for each shard in the snapshot. [role="screenshot"] image:management/snapshot-restore/images/snapshot-restore.png["Snapshot details"] @@ -89,23 +102,28 @@ image:management/snapshot-restore/images/snapshot-restore.png["Snapshot details" [[kib-snapshot-policy]] === Create a snapshot lifecycle policy -You can create policies to schedule automatic snapshots of your cluster. -{ref}/snapshot-lifecycle-management-api.html[Snapshot lifecycle policies] are related -to {ref}/index-lifecycle-management.html[index lifecycle policies]. -However, where an index lifecycle policy applies to a single index, -a snapshot lifecycle policy can span multiple indices. - -For an overview of your policies, open the *Policies* view. -You can drill down into each policy to examine its settings and last successful and failed run. - -If you don’t have any policies, use the *Create policy* wizard. -You’ll define the snapshots and repository, when to take snapshots, and -the settings, such as which indices the snapshot should contain. +Use a {ref}/snapshot-lifecycle-management-api.html[snapshot lifecycle policy] +to automate the creation and deletion +of cluster snapshots. Taking automatic snapshots: + +* Ensures your {es} indices and clusters are backed up on a regular basis +* Ensures a recent and relevant snapshot is available if a situation +arises where a cluster needs to be recovered +* Allows you to manage your snapshots in {kib}, instead of using a +third-party tool + +If you don’t have any snapshot policies, follow the +*Create policy* wizard. It walks you through defining +when and where to take snapshots, the settings you want, +and how long to retain snapshots. [role="screenshot"] -image:management/snapshot-restore/images/create-policy.png["Snapshot details"] +image:management/snapshot-restore/images/snapshot-retention.png["Snapshot details"] + +An overview of your policies is on the *Policies* view. +You can drill down into each policy to examine its settings and last successful and failed run. -You can perform the following actions on a policy: +You can perform the following actions on a snapshot policy: * *Run* a policy immediately without waiting for the scheduled time. This action is useful before an upgrade or before performing maintenance on indices. @@ -113,6 +131,9 @@ This action is useful before an upgrade or before performing maintenance on indi * *Delete* a policy to prevent any future snapshots from being taken. This action does not cancel any currently ongoing snapshots or remove any previously taken snapshots. +[role="screenshot"] +image:management/snapshot-restore/images/create-policy.png["Snapshot details"] + [float] [[kib-delete-snapshot]] === Delete a snapshot @@ -123,16 +144,25 @@ Find the snapshot in the *Snapshots* view and click the trash icon in the and then click *Delete snapshots*. [[snapshot-repositories-example]] -[float] -=== Example: Register a shared file system repository -This example shows how to register a shared file system repository -and store snapshots. +[role="xpack"] +[[snapshot-restore-tutorial]] +=== Tutorial: Snapshot and Restore -[float] -==== Register the repository location -You must register the location of the repository in the `path.repo` setting on +Ready to try *Snapshot and Restore*? In this tutorial, you'll learn to: + +* Register a repository +* Add snapshots to the repository +* Create a snapshot lifecycle policy +* Restore a snapshot + +==== Before you begin + +This example shows you how to register a shared file system repository +and store snapshots. +Before you begin, you must register the location of the repository in the +{ref}/modules-snapshots.html#_shared_file_system_repository[path.repo] setting on your master and data nodes. You can do this in one of two ways: * Edit your `elasticsearch.yml` to include the `path.repo` setting. @@ -142,30 +172,26 @@ your master and data nodes. You can do this in one of two ways: `bin/elasticsearch -E path.repo=/tmp/es-backups` [float] -==== Register the repository +[[register-repo-example]] +==== Register a repository Use *Snapshot and Restore* to register the repository where your snapshots will live. . Go to *Management > Elasticsearch > Snapshot and Restore*. -. Open the *Repositories* view. -. Click *Register a repository*. +. Click *Register a repository* in either the introductory message or *Repository view*. . Enter a name for your repository, for example, `my_backup`. -. Set *Repository type* to Shared file system. +. Select *Shared file system*. + [role="screenshot"] image:management/snapshot-restore/images/register_repo.png["Register repository"] . Click *Next*. -. In *Location*, enter the path to the snapshot repository, `/tmp/es-backups`. -. In *Chunk size*, enter 100mb so that snapshot files are not bigger than that size. -. Use the defaults for all other fields. -. Click *Register*. +. In *File system location*, enter the path to the snapshot repository, `/tmp/es-backups`. +. In *Chunk size*, enter `100mb` so that snapshot files are not bigger than that size. +. Use the defaults for all other fields, and then click *Register*. + Your new repository is listed on the *Repositories* view. -+ -. Click the respository and inspect its details. -+ The repository currently doesn’t have any snapshots. @@ -174,19 +200,105 @@ The repository currently doesn’t have any snapshots. Use the {ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API] to create a snapshot. . Go to *Dev Tools > Console*. -. Create the snapshot. +. Create the snapshot: ++ +[source,js] +PUT /_snapshot/my_backup/2019-04-25_snapshot?wait_for_completion=true + In this example, the snapshot name is `2019-04-25_snapshot`. You can also use {ref}//date-math-index-names.html[date math expression] for the snapshot name. + [role="screenshot"] image:management/snapshot-restore/images/create_snapshot.png["Create snapshot"] -+ -. Open *Snapshot and Restore*. + +. Return to *Snapshot and Restore*. + Your new snapshot is available in the *Snapshots* view. +[[create-policy-example]] +==== Create a snapshot lifecycle policy + +Now you'll automate the creation and deletion of snapshots +using the repository created in the previous example. + +. Open the *Policies* view. +. Click *Create a policy*. ++ +[role="screenshot"] +image:management/snapshot-restore/images/create-policy-example.png["Create policy wizard"] + +. As you walk through the wizard, enter the following values: ++ +|=== +|*Logistics* | + +|Policy name +|`daily-snapshots` + +|Snapshot name +|`` + +|Schedule +|Every day at 1:30 a.m. + +|Repository +|`my_backup` + +|*Snapshot settings* | +|Indices +|Select the indices to back up. By default, all indices, including system indices, are backed up. +|All other settings +|Use the defaults. +|*Snapshot retention* | + +|Expiration +|`30 days` + +|Snapshots to retain +|Minimum count: `5`, Maximum count: `50` +|=== + +. Review your input, and then click *Create policy*. ++ +Your new policy is listed in the *Policies* view, and you see a summary of its details. + +[[restore-snapshot-example]] +==== Restore a snapshot +Finally, you'll restore indices from an existing snapshot. + +. In the *Snapshots* view, find the snapshot you want to restore, for example `2019-04-25_snapshot`. +. Click the restore icon in the *Actions* column. +. As you walk through the wizard, enter the following values: ++ +|=== +|*Logistics* | + +|Indices +|Toggle to choose specific indices to restore, or leave in place to restore all indices. + +|Rename indices +|Toggle to give your restored indices new names, or leave in place to restore under original index names. + +|All other fields +|Use the defaults. + +|*Index settings* | + +|Modify index settings +|Toggle to overwrite index settings when they are restored, +or leave in place to keep existing settings. + +|Reset index settings +|Toggle to reset index settings back to the default when they are restored, +or leave in place to keep existing settings. +|=== + +. Review your restore settings, and then click *Restore snapshot*. ++ +The operation loads for a few seconds, +and then you’re navigated to *Restore Status*, +where you can monitor the status of your restored indices. diff --git a/docs/user/security/api-keys/images/api-key-invalidate.png b/docs/user/security/api-keys/images/api-key-invalidate.png new file mode 100755 index 0000000000000..c925679ab24bc Binary files /dev/null and b/docs/user/security/api-keys/images/api-key-invalidate.png differ diff --git a/docs/user/security/api-keys/images/api-keys.png b/docs/user/security/api-keys/images/api-keys.png new file mode 100755 index 0000000000000..df74f245676d9 Binary files /dev/null and b/docs/user/security/api-keys/images/api-keys.png differ diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc new file mode 100644 index 0000000000000..c00f58cf598e3 --- /dev/null +++ b/docs/user/security/api-keys/index.asciidoc @@ -0,0 +1,86 @@ +[role="xpack"] +[[api-keys]] +=== API Keys + + +API keys enable you to create secondary credentials so that you can send +requests on behalf of the user. Secondary credentials have +the same or lower access rights. + +For example, if you extract data from an {es} cluster on a daily +basis, you might create an API key tied to your credentials, +configure it with minimum access, +and then put the API credentials into a cron job. +Or, you might create API keys to automate ingestion of new data from +remote sources, without a live user interaction. + +You can create API keys from the {kib} Console. To view and invalidate +API keys, use *Management > Security > API Keys*. + +[role="screenshot"] +image:user/security/api-keys/images/api-keys.png["API Keys UI"] + +[float] +[[api-keys-service]] +=== {es} API key service + +The {es} API key service is automatically enabled when you configure +{ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. +This ensures that clients are unable to send API keys in clear-text. + +When HTTPS connections are not enabled between {kib} and {es}, +you cannot create or manage API keys, and you get an error message. +For more information, see the +{ref}/security-api-create-api-key.html[{es} API key documentation], +or contact your system administrator. + +[float] +[[api-keys-security-privileges]] +=== Security privileges + +You must have the `manage_security`, `manage_api_key`, or the `manage_own_api_key` +cluster privileges to use API keys in {kib}. You can manage roles in +*Management > Security > Roles*, or use the <>. + + +[float] +[[create-api-key]] +=== Create an API key +You can {ref}/security-api-create-api-key.html[create an API key] from +the Kibana Console. For example: + +[source,js] +POST /_security/api_key +{ + "name": "my_api_key", + "expiration": "1d" +} + +This creates an API key with the name `my_api_key` that +expires after one day. API key names must be globally unique. +An expiration date is optional and follows {ref}/common-options.html#time-units[{es} time unit format]. +When an expiration is not provided, the API key does not expire. + +[float] +[[view-api-keys]] +=== View and invalidate API keys +The *API Keys* UI lists your API keys, including the name, date created, +and expiration date. If an API key expires, its status changes from `Active` to `Expired`. + +If you have `manage_security` or `manage_api_key` permissions, +you can view the API keys of all users, and see which API key was +created by which user in which realm. +If you have only the `manage_own_api_key` permission, you see only a list of your own keys. + +You can invalidate API keys individually or in bulk. +Invalidated keys are deleted in batch after seven days. + +[role="screenshot"] +image:user/security/api-keys/images/api-key-invalidate.png["API Keys invalidate"] + +You cannot modify an API key. If you need additional privileges, +you must create a new key with the desired configuration and invalidate the old key. + + + + diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index 7b7e38d610843..f57d1bcd3bc2a 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -36,3 +36,5 @@ cause Kibana's authorization to behave unexpectedly. include::authorization/index.asciidoc[] include::authorization/kibana-privileges.asciidoc[] +include::api-keys/index.asciidoc[] + diff --git a/package.json b/package.json index d64307ade9247..289c2e8b0a8d0 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "**/typescript": "3.5.3", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", - "**/babel-plugin-inline-react-svg/svgo/js-yaml": "^3.13.1", "**/image-diff/gm/debug": "^2.6.9" }, "workspaces": { @@ -107,7 +106,7 @@ "dependencies": { "@babel/core": "^7.5.5", "@babel/register": "^7.5.5", - "@elastic/charts": "^13.5.7", + "@elastic/charts": "^13.5.9", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", "@elastic/eui": "14.7.0", @@ -237,7 +236,6 @@ "rxjs": "^6.2.1", "script-loader": "0.7.2", "semver": "^5.5.0", - "stream-stream": "^1.2.6", "style-it": "^2.1.3", "style-loader": "0.23.1", "symbol-observable": "^1.2.0", @@ -247,8 +245,6 @@ "tinygradient": "0.4.3", "tinymath": "1.2.1", "topojson-client": "3.0.0", - "trunc-html": "1.1.2", - "trunc-text": "1.0.2", "tslib": "^1.9.3", "type-detect": "^4.0.8", "ui-select": "0.19.8", @@ -312,7 +308,6 @@ "@types/has-ansi": "^3.0.0", "@types/history": "^4.7.3", "@types/hoek": "^4.1.3", - "@types/humps": "^1.1.2", "@types/jest": "^24.0.18", "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", diff --git a/packages/kbn-expect/expect.js b/packages/kbn-expect/expect.js index 8dc8af4cab894..bc75d19d2ab1f 100644 --- a/packages/kbn-expect/expect.js +++ b/packages/kbn-expect/expect.js @@ -98,6 +98,9 @@ Assertion.prototype.assert = function (truth, msg, error, expected) { if (!ok) { err = new Error(msg.call(this)); + if (this.customMsg) { + err.message = this.customMsg; + } if (arguments.length > 3) { err.actual = this.obj; err.expected = expected; @@ -217,7 +220,10 @@ Assertion.prototype.empty = function () { */ Assertion.prototype.be = -Assertion.prototype.equal = function (obj) { +Assertion.prototype.equal = function (obj, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } this.assert( obj === this.obj , function(){ return 'expected ' + i(this.obj) + ' to equal ' + i(obj) } @@ -231,7 +237,10 @@ Assertion.prototype.equal = function (obj) { * @api public */ -Assertion.prototype.eql = function (obj) { +Assertion.prototype.eql = function (obj, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } this.assert( expect.eql(this.obj, obj) , function(){ return 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) } @@ -248,7 +257,10 @@ Assertion.prototype.eql = function (obj) { * @api public */ -Assertion.prototype.within = function (start, finish) { +Assertion.prototype.within = function (start, finish, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } var range = start + '..' + finish; this.assert( this.obj >= start && this.obj <= finish @@ -298,7 +310,10 @@ Assertion.prototype.an = function (type) { */ Assertion.prototype.greaterThan = -Assertion.prototype.above = function (n) { +Assertion.prototype.above = function (n, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } this.assert( this.obj > n , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n } @@ -314,7 +329,10 @@ Assertion.prototype.above = function (n) { */ Assertion.prototype.lessThan = -Assertion.prototype.below = function (n) { +Assertion.prototype.below = function (n, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } this.assert( this.obj < n , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n } @@ -329,7 +347,10 @@ Assertion.prototype.below = function (n) { * @api public */ -Assertion.prototype.match = function (regexp) { +Assertion.prototype.match = function (regexp, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } this.assert( regexp.exec(this.obj) , function(){ return 'expected ' + i(this.obj) + ' to match ' + regexp } @@ -344,7 +365,10 @@ Assertion.prototype.match = function (regexp) { * @api public */ -Assertion.prototype.length = function (n) { +Assertion.prototype.length = function (n, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } expect(this.obj).to.have.property('length'); var len = this.obj.length; this.assert( @@ -410,7 +434,10 @@ Assertion.prototype.property = function (name, val) { */ Assertion.prototype.string = -Assertion.prototype.contain = function (obj) { +Assertion.prototype.contain = function (obj, msg) { + if (typeof(msg) === 'string') { + this.customMsg = msg; + } if ('string' == typeof this.obj) { this.assert( ~this.obj.indexOf(obj) diff --git a/packages/kbn-expect/expect.js.d.ts b/packages/kbn-expect/expect.js.d.ts index 2062dea686500..b957a1f9ab109 100644 --- a/packages/kbn-expect/expect.js.d.ts +++ b/packages/kbn-expect/expect.js.d.ts @@ -59,12 +59,12 @@ interface Assertion { /** * Checks if the obj exactly equals another. */ - equal(obj: any): Assertion; + equal(obj: any, msg?: string): Assertion; /** * Checks if the obj sortof equals another. */ - eql(obj: any): Assertion; + eql(obj: any, msg?: string): Assertion; /** * Assert within start to finish (inclusive). @@ -72,7 +72,7 @@ interface Assertion { * @param start * @param finish */ - within(start: number, finish: number): Assertion; + within(start: number, finish: number, msg?: string): Assertion; /** * Assert typeof. @@ -87,36 +87,36 @@ interface Assertion { /** * Assert numeric value above n. */ - greaterThan(n: number): Assertion; + greaterThan(n: number, msg?: string): Assertion; /** * Assert numeric value above n. */ - above(n: number): Assertion; + above(n: number, msg?: string): Assertion; /** * Assert numeric value below n. */ - lessThan(n: number): Assertion; + lessThan(n: number, msg?: string): Assertion; /** * Assert numeric value below n. */ - below(n: number): Assertion; + below(n: number, msg?: string): Assertion; /** * Assert string value matches regexp. * * @param regexp */ - match(regexp: RegExp): Assertion; + match(regexp: RegExp, msg?: string): Assertion; /** * Assert property "length" exists and has value of n. * * @param n */ - length(n: number): Assertion; + length(n: number, msg?: string): Assertion; /** * Assert property name exists, with optional val. @@ -129,14 +129,14 @@ interface Assertion { /** * Assert that string contains str. */ - contain(str: string): Assertion; - string(str: string): Assertion; + contain(str: string, msg?: string): Assertion; + string(str: string, msg?: string): Assertion; /** * Assert that the array contains obj. */ - contain(obj: any): Assertion; - string(obj: any): Assertion; + contain(obj: any, msg?: string): Assertion; + string(obj: any, msg?: string): Assertion; /** * Assert exact keys or inclusion of keys by using the `.own` modifier. diff --git a/renovate.json5 b/renovate.json5 index deb513d57c85e..2d068f55bc61a 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -401,14 +401,6 @@ '@types/history', ], }, - { - groupSlug: 'humps', - groupName: 'humps related packages', - packageNames: [ - 'humps', - '@types/humps', - ], - }, { groupSlug: 'jquery', groupName: 'jquery related packages', diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 8d56f6406ef59..a67593c02a593 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -113,7 +113,7 @@ export default class ClusterManager { ...scanDirs, ]; - const extraIgnores = scanDirs + const pluginInternalDirsIgnore = scanDirs .map(scanDir => resolve(scanDir, '*')) .concat(pluginPaths) .reduce( @@ -124,16 +124,11 @@ export default class ClusterManager { resolve(path, 'target'), resolve(path, 'scripts'), resolve(path, 'docs'), - resolve(path, 'src/legacy/server/sass/__tmp__'), - resolve(path, 'legacy/plugins/reporting/.chromium'), - resolve(path, 'legacy/plugins/siem/cypress'), - resolve(path, 'legacy/plugins/apm/cypress'), - resolve(path, 'x-pack/legacy/plugins/canvas/canvas_plugin_src') // prevents server from restarting twice for Canvas plugin changes ), [] ); - this.setupWatching(extraPaths, extraIgnores); + this.setupWatching(extraPaths, pluginInternalDirsIgnore); } else this.startCluster(); } @@ -170,7 +165,7 @@ export default class ClusterManager { .then(() => opn(openUrl)); } - setupWatching(extraPaths, extraIgnores) { + setupWatching(extraPaths, pluginInternalDirsIgnore) { const chokidar = require('chokidar'); const { fromRoot } = require('../../legacy/utils'); @@ -187,12 +182,21 @@ export default class ClusterManager { ...extraPaths, ].map(path => resolve(path)); + const ignorePaths = [ + fromRoot('src/legacy/server/sass/__tmp__'), + fromRoot('x-pack/legacy/plugins/reporting/.chromium'), + fromRoot('x-pack/legacy/plugins/siem/cypress'), + fromRoot('x-pack/legacy/plugins/apm/cypress'), + fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src') // prevents server from restarting twice for Canvas plugin changes + ]; + this.watcher = chokidar.watch(uniq(watchPaths), { cwd: fromRoot('.'), ignored: [ /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, /\.test\.(js|ts)$/, - ...extraIgnores, + ...pluginInternalDirsIgnore, + ...ignorePaths, 'plugins/java_languageserver' ], }); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 6a12b587085e1..018a91be4c3d1 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1111,6 +1111,7 @@ import { npStart: { core } } from 'ui/new_platform'; | `ui/notify` | [`core.notifications`](/docs/development/core/public/kibana-plugin-public.notificationsstart.md) and [`core.overlays`](/docs/development/core/public/kibana-plugin-public.overlaystart.md) | Toast messages are in `notifications`, banners are in `overlays`. May be combined later. | | `ui/routes` | -- | There is no global routing mechanism. Each app [configures its own routing](/rfcs/text/0004_application_service_mounting.md#complete-example). | | `ui/saved_objects` | [`core.savedObjects`](/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md) | Client API is the same | +| `ui/doc_title` | [`core.chrome.docTitle`](/docs/development/core/public/kibana-plugin-public.chromedoctitle.md) | | _See also: [Public's CoreStart API Docs](/docs/development/core/public/kibana-plugin-public.corestart.md)_ @@ -1131,7 +1132,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | | `import 'ui/query_bar'` | `import { QueryBar, QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | -| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../kibana_react/public'` | Directive is still available in `ui/kbn_top_nav`. | +| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | | `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | | `core_plugins/interpreter` | `data.expressions` | still in progress | | `ui/courier` | `data.search` | still in progress | diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 3775989c5126b..6f61ee9dc21ba 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -43,6 +43,13 @@ const createStartContractMock = () => { get: jest.fn(), get$: jest.fn(), }, + docTitle: { + change: jest.fn(), + reset: jest.fn(), + __legacy: { + setBaseTitle: jest.fn(), + }, + }, navControls: { registerLeft: jest.fn(), registerRight: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 71279ad6fed03..87389d2c10f03 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -33,10 +33,11 @@ import { HttpStart } from '../http'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { NavControlsService, ChromeNavControls } from './nav_controls'; +import { DocTitleService, ChromeDocTitle } from './doc_title'; import { LoadingIndicator, HeaderWrapper as Header } from './ui'; import { DocLinksStart } from '../doc_links'; -export { ChromeNavControls, ChromeRecentlyAccessed }; +export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; @@ -82,6 +83,7 @@ export class ChromeService { private readonly navControls = new NavControlsService(); private readonly navLinks = new NavLinksService(); private readonly recentlyAccessed = new RecentlyAccessedService(); + private readonly docTitle = new DocTitleService(); constructor(private readonly params: ConstructorParams) {} @@ -106,6 +108,7 @@ export class ChromeService { const navControls = this.navControls.start(); const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); + const docTitle = this.docTitle.start({ document: window.document }); if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( @@ -119,6 +122,7 @@ export class ChromeService { navControls, navLinks, recentlyAccessed, + docTitle, getHeaderComponent: () => ( @@ -259,6 +263,8 @@ export interface ChromeStart { navControls: ChromeNavControls; /** {@inheritdoc ChromeRecentlyAccessed} */ recentlyAccessed: ChromeRecentlyAccessed; + /** {@inheritdoc ChromeDocTitle} */ + docTitle: ChromeDocTitle; /** * Sets the current app's title diff --git a/src/core/public/chrome/doc_title/doc_title_service.test.ts b/src/core/public/chrome/doc_title/doc_title_service.test.ts new file mode 100644 index 0000000000000..763e8c9ebd74a --- /dev/null +++ b/src/core/public/chrome/doc_title/doc_title_service.test.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 { DocTitleService } from './doc_title_service'; + +describe('DocTitleService', () => { + const defaultTitle = 'KibanaTest'; + const document = { title: '' }; + + const getStart = (title: string = defaultTitle) => { + document.title = title; + return new DocTitleService().start({ document }); + }; + + beforeEach(() => { + document.title = defaultTitle; + }); + + describe('#change()', () => { + it('changes the title of the document', async () => { + getStart().change('TitleA'); + expect(document.title).toEqual('TitleA - KibanaTest'); + }); + + it('appends the baseTitle to the title', async () => { + const start = getStart('BaseTitle'); + start.change('TitleA'); + expect(document.title).toEqual('TitleA - BaseTitle'); + start.change('TitleB'); + expect(document.title).toEqual('TitleB - BaseTitle'); + }); + + it('accepts string arrays as input', async () => { + const start = getStart(); + start.change(['partA', 'partB']); + expect(document.title).toEqual(`partA - partB - ${defaultTitle}`); + start.change(['partA', 'partB', 'partC']); + expect(document.title).toEqual(`partA - partB - partC - ${defaultTitle}`); + }); + }); + + describe('#reset()', () => { + it('resets the title to the initial value', async () => { + const start = getStart('InitialTitle'); + start.change('TitleA'); + expect(document.title).toEqual('TitleA - InitialTitle'); + start.reset(); + expect(document.title).toEqual('InitialTitle'); + }); + }); + + describe('#__legacy.setBaseTitle()', () => { + it('allows to change the baseTitle after startup', async () => { + const start = getStart('InitialTitle'); + start.change('WithInitial'); + expect(document.title).toEqual('WithInitial - InitialTitle'); + start.__legacy.setBaseTitle('NewBaseTitle'); + start.change('WithNew'); + expect(document.title).toEqual('WithNew - NewBaseTitle'); + start.reset(); + expect(document.title).toEqual('NewBaseTitle'); + }); + }); +}); diff --git a/src/core/public/chrome/doc_title/doc_title_service.ts b/src/core/public/chrome/doc_title/doc_title_service.ts new file mode 100644 index 0000000000000..9453abe54de66 --- /dev/null +++ b/src/core/public/chrome/doc_title/doc_title_service.ts @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { compact, flattenDeep, isString } from 'lodash'; + +interface StartDeps { + document: { title: string }; +} + +/** + * APIs for accessing and updating the document title. + * + * @example + * How to change the title of the document + * ```ts + * chrome.docTitle.change('My application') + * ``` + * + * @example + * How to reset the title of the document to it's initial value + * ```ts + * chrome.docTitle.reset() + * ``` + * + * @public + * */ +export interface ChromeDocTitle { + /** + * Changes the current document title. + * + * @example + * How to change the title of the document + * ```ts + * chrome.docTitle.change('My application title') + * chrome.docTitle.change(['My application', 'My section']) + * ``` + * + * @param newTitle The new title to set, either a string or string array + */ + change(newTitle: string | string[]): void; + /** + * Resets the document title to it's initial value. + * (meaning the one present in the title meta at application load.) + */ + reset(): void; + + /** @internal */ + __legacy: { + setBaseTitle(baseTitle: string): void; + }; +} + +const defaultTitle: string[] = []; +const titleSeparator = ' - '; + +/** @internal */ +export class DocTitleService { + private document = { title: '' }; + private baseTitle = ''; + + public start({ document }: StartDeps): ChromeDocTitle { + this.document = document; + this.baseTitle = document.title; + + return { + change: (title: string | string[]) => { + this.applyTitle(title); + }, + reset: () => { + this.applyTitle(defaultTitle); + }, + __legacy: { + setBaseTitle: baseTitle => { + this.baseTitle = baseTitle; + }, + }, + }; + } + + private applyTitle(title: string | string[]) { + this.document.title = this.render(title); + } + + private render(title: string | string[]) { + const parts = [...(isString(title) ? [title] : title), this.baseTitle]; + // ensuring compat with legacy that might be passing nested arrays + return compact(flattenDeep(parts)).join(titleSeparator); + } +} diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/index.ts b/src/core/public/chrome/doc_title/index.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/discover/doc_viewer/index.ts rename to src/core/public/chrome/doc_title/index.ts index 8ce94f24128df..b070d01953f7a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/index.ts +++ b/src/core/public/chrome/doc_title/index.ts @@ -17,6 +17,4 @@ * under the License. */ -import './doc_viewer_directive'; - -export * from './doc_viewer'; +export * from './doc_title_service'; diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index 6e03f9e023983..b220a81f775f8 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -29,3 +29,4 @@ export { export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields } from './nav_links'; export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './recently_accessed'; export { ChromeNavControl, ChromeNavControls } from './nav_controls'; +export { ChromeDocTitle } from './doc_title'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 24201ff0253cb..7391cf7f9454c 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -45,6 +45,7 @@ import { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields, + ChromeDocTitle, ChromeStart, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, @@ -250,6 +251,7 @@ export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields, + ChromeDocTitle, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, ChromeStart, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 11a1b5c0d1d9b..416fb13cbb73e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -105,6 +105,16 @@ export interface ChromeBrand { // @public (undocumented) export type ChromeBreadcrumb = Breadcrumb; +// @public +export interface ChromeDocTitle { + // @internal (undocumented) + __legacy: { + setBaseTitle(baseTitle: string): void; + }; + change(newTitle: string | string[]): void; + reset(): void; +} + // @public (undocumented) export type ChromeHelpExtension = (element: HTMLDivElement) => () => void; @@ -186,6 +196,7 @@ export interface ChromeRecentlyAccessedHistoryItem { // @public export interface ChromeStart { addApplicationClass(className: string): void; + docTitle: ChromeDocTitle; getApplicationClasses$(): Observable; getBadge$(): Observable; getBrand$(): Observable; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e0d230006d587..c3f63c7c26e97 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -139,6 +139,7 @@ export { SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, + SavedObjectsBulkUpdateOptions, SavedObjectsBulkResponse, SavedObjectsBulkUpdateResponse, SavedObjectsClient, @@ -166,6 +167,7 @@ export { SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, + SavedObjectsDeleteOptions, } from './saved_objects'; export { @@ -184,6 +186,7 @@ export { SavedObjectAttributeSingle, SavedObjectReference, SavedObjectsBaseOptions, + MutatingOperationRefreshSetting, SavedObjectsClientContract, SavedObjectsFindOptions, SavedObjectsMigrationVersion, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index fb703c6c35008..deb5984564db1 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -29,7 +29,7 @@ export { configServiceMock } from './config/config_service.mock'; export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; -export { SavedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; +export { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export function pluginInitializerContextConfigMock(config: T) { 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 1a2a843ebb2b8..9a3449b65a941 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 @@ -18,7 +18,7 @@ */ import { getSortedObjectsForExport } from './get_sorted_objects_for_export'; -import { SavedObjectsClientMock } from '../service/saved_objects_client.mock'; +import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '../../../../legacy/utils/streams'; @@ -27,7 +27,7 @@ async function readStreamToCompletion(stream: Readable) { } describe('getSortedObjectsForExport()', () => { - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); afterEach(() => { savedObjectsClient.find.mockReset(); diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index 57feebbf67ccd..a571f62e3d1c1 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -18,7 +18,7 @@ */ import { SavedObject } from '../types'; -import { SavedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../mocks'; import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies'; describe('getObjectReferencesToFetch()', () => { @@ -109,7 +109,7 @@ describe('getObjectReferencesToFetch()', () => { }); describe('injectNestedDependencies', () => { - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); afterEach(() => { jest.resetAllMocks(); 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 df95fb75f0f4f..f0719cbf4c829 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 @@ -20,7 +20,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; import { importSavedObjects } from './import_saved_objects'; -import { SavedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../mocks'; const emptyResponse = { saved_objects: [], @@ -63,7 +63,7 @@ describe('importSavedObjects()', () => { references: [], }, ]; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { jest.resetAllMocks(); 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 6aab8ef5adf9e..c522d76f1ff04 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 @@ -20,7 +20,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; import { resolveImportErrors } from './resolve_import_errors'; -import { SavedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../mocks'; describe('resolveImportErrors()', () => { const savedObjects: SavedObject[] = [ @@ -63,7 +63,7 @@ describe('resolveImportErrors()', () => { ], }, ]; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { jest.resetAllMocks(); diff --git a/src/core/server/saved_objects/import/validate_references.test.ts b/src/core/server/saved_objects/import/validate_references.test.ts index 269cd3055b047..6642cf149eda9 100644 --- a/src/core/server/saved_objects/import/validate_references.test.ts +++ b/src/core/server/saved_objects/import/validate_references.test.ts @@ -18,10 +18,10 @@ */ import { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; -import { SavedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../mocks'; describe('getNonExistingReferenceAsKeys()', () => { - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { jest.resetAllMocks(); @@ -222,7 +222,7 @@ describe('getNonExistingReferenceAsKeys()', () => { }); describe('validateReferences()', () => { - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { jest.resetAllMocks(); diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index e514210752f51..9941bbd03f5dc 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -417,6 +417,32 @@ describe('SavedObjectsRepository', () => { }); }); + it('defaults to a refresh setting of `wait_for`', async () => { + await savedObjectsRepository.create('index-pattern', { + id: 'logstash-*', + title: 'Logstash', + }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: 'wait_for' + }); + }); + + it('accepts custom refresh settings', async () => { + await savedObjectsRepository.create('index-pattern', { + id: 'logstash-*', + title: 'Logstash', + }, { + refresh: true + }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: true + }); + }); + it('should use create action if ID defined and overwrite=false', async () => { await savedObjectsRepository.create( 'index-pattern', @@ -645,6 +671,61 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); + it('defaults to a refresh setting of `wait_for`', async () => { + callAdminCluster.mockReturnValue({ + items: [ + { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, + ], + }); + + await savedObjectsRepository.bulkCreate([ + { + type: 'config', + id: 'one', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + } + ]); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: 'wait_for' + }); + }); + + it('accepts a custom refresh setting', async () => { + callAdminCluster.mockReturnValue({ + items: [ + { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, + { create: { type: 'index-pattern', id: 'config:two', _primary_term: 1, _seq_no: 1 } }, + ], + }); + + await savedObjectsRepository.bulkCreate([ + { + type: 'config', + id: 'one', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }, + { + type: 'index-pattern', + id: 'two', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }, + ], { + refresh: true + }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: true + }); + }); + it('migrates the docs', async () => { callAdminCluster.mockReturnValue({ items: [ @@ -1087,6 +1168,28 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); + + it('defaults to a refresh setting of `wait_for`', async () => { + callAdminCluster.mockReturnValue({ result: 'deleted' }); + await savedObjectsRepository.delete('globaltype', 'logstash-*'); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: 'wait_for' + }); + }); + + it(`accepts a custom refresh setting`, async () => { + callAdminCluster.mockReturnValue({ result: 'deleted' }); + await savedObjectsRepository.delete('globaltype', 'logstash-*', { + refresh: false + }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: false, + }); + }); }); describe('#deleteByNamespace', () => { @@ -1126,6 +1229,26 @@ describe('SavedObjectsRepository', () => { refresh: 'wait_for', }); }); + + it('defaults to a refresh setting of `wait_for`', async () => { + callAdminCluster.mockReturnValue(deleteByQueryResults); + await savedObjectsRepository.deleteByNamespace('my-namespace'); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: 'wait_for', + }); + }); + + it('accepts a custom refresh setting', async () => { + callAdminCluster.mockReturnValue(deleteByQueryResults); + await savedObjectsRepository.deleteByNamespace('my-namespace', { refresh: true }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: true, + }); + }); }); describe('#find', () => { @@ -1984,6 +2107,40 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); + + it('defaults to a refresh setting of `wait_for`', async () => { + await savedObjectsRepository.update( + 'globaltype', + 'foo', + { + name: 'bar', + } + ); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: 'wait_for' + }); + }); + + it('accepts a custom refresh setting', async () => { + await savedObjectsRepository.update( + 'globaltype', + 'foo', + { + name: 'bar', + }, + { + refresh: true, + namespace: 'foo-namespace', + } + ); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: true + }); + }); }); describe('#bulkUpdate', () => { @@ -2284,6 +2441,44 @@ describe('SavedObjectsRepository', () => { }); }); + it('defaults to a refresh setting of `wait_for`', async () => { + const objects = [ + { + type: 'index-pattern', + id: `logstash-no-ref`, + attributes: { title: `Testing no-ref` }, + references: [] + } + ]; + + mockValidResponse(objects); + + await savedObjectsRepository.bulkUpdate(objects); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ refresh: 'wait_for' }); + }); + + it('accepts a custom refresh setting', async () => { + const objects = [ + { + type: 'index-pattern', + id: `logstash-no-ref`, + attributes: { title: `Testing no-ref` }, + references: [] + } + ]; + + mockValidResponse(objects); + + await savedObjectsRepository.bulkUpdate(objects, { refresh: true }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ refresh: true }); + }); + it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { const objects = [ @@ -2526,6 +2721,29 @@ describe('SavedObjectsRepository', () => { }); }); + it('defaults to a refresh setting of `wait_for`', async () => { + await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { + namespace: 'foo-namespace' + }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: 'wait_for' + }); + }); + + it('accepts a custom refresh setting', async () => { + await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { + namespace: 'foo-namespace', + refresh: true + }); + + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ + refresh: true + }); + }); + it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum', { namespace: 'foo-namespace', diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 179aa6fffc7de..54b9938decb0a 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -40,6 +40,9 @@ import { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, SavedObjectsBulkUpdateObject, + SavedObjectsBulkUpdateOptions, + SavedObjectsDeleteOptions, + SavedObjectsDeleteByNamespaceOptions, } from '../saved_objects_client'; import { SavedObject, @@ -47,6 +50,7 @@ import { SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, + MutatingOperationRefreshSetting, } from '../../types'; import { validateConvertFilterToKueryNode } from './filter_utils'; @@ -83,8 +87,12 @@ export interface SavedObjectsRepositoryOptions { export interface IncrementCounterOptions extends SavedObjectsBaseOptions { migrationVersion?: SavedObjectsMigrationVersion; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; } +const DEFAULT_REFRESH_SETTING = 'wait_for'; + export class SavedObjectsRepository { private _migrator: KibanaMigrator; private _index: string; @@ -154,7 +162,14 @@ export class SavedObjectsRepository { attributes: T, options: SavedObjectsCreateOptions = {} ): Promise> { - const { id, migrationVersion, overwrite = false, namespace, references = [] } = options; + const { + id, + migrationVersion, + overwrite = false, + namespace, + references = [], + refresh = DEFAULT_REFRESH_SETTING, + } = options; if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); @@ -179,7 +194,7 @@ export class SavedObjectsRepository { const response = await this._writeToCluster(method, { id: raw._id, index: this.getIndexForType(type), - refresh: 'wait_for', + refresh, body: raw._source, }); @@ -210,7 +225,7 @@ export class SavedObjectsRepository { objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> { - const { namespace, overwrite = false } = options; + const { namespace, overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); const bulkCreateParams: object[] = []; @@ -256,7 +271,7 @@ export class SavedObjectsRepository { }); const esResponse = await this._writeToCluster('bulk', { - refresh: 'wait_for', + refresh, body: bulkCreateParams, }); @@ -308,17 +323,17 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} */ - async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}): Promise<{}> { + async delete(type: string, id: string, options: SavedObjectsDeleteOptions = {}): Promise<{}> { if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(); } - const { namespace } = options; + const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; const response = await this._writeToCluster('delete', { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), - refresh: 'wait_for', + refresh, ignore: [404], }); @@ -345,11 +360,16 @@ export class SavedObjectsRepository { * @param {string} namespace * @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures } */ - async deleteByNamespace(namespace: string): Promise { + async deleteByNamespace( + namespace: string, + options: SavedObjectsDeleteByNamespaceOptions = {} + ): Promise { if (!namespace || typeof namespace !== 'string') { throw new TypeError(`namespace is required, and must be a string`); } + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); const typesToDelete = allTypes.filter(type => !this._schema.isNamespaceAgnostic(type)); @@ -357,7 +377,7 @@ export class SavedObjectsRepository { const esOptions = { index: this.getIndicesForTypes(typesToDelete), ignore: [404], - refresh: 'wait_for', + refresh, body: { conflicts: 'proceed', ...getSearchDsl(this._mappings, this._schema, { @@ -626,7 +646,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { version, namespace, references } = options; + const { version, namespace, references, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); @@ -643,7 +663,7 @@ export class SavedObjectsRepository { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), ...(version && decodeRequestVersion(version)), - refresh: 'wait_for', + refresh, ignore: [404], body: { doc, @@ -675,7 +695,7 @@ export class SavedObjectsRepository { */ async bulkUpdate( objects: Array>, - options: SavedObjectsBaseOptions = {} + options: SavedObjectsBulkUpdateOptions = {} ): Promise> { const time = this._getCurrentTime(); const bulkUpdateParams: object[] = []; @@ -729,9 +749,10 @@ export class SavedObjectsRepository { return { tag: 'Right' as 'Right', value: expectedResult }; }); + const { refresh = DEFAULT_REFRESH_SETTING } = options; const esResponse = bulkUpdateParams.length ? await this._writeToCluster('bulk', { - refresh: 'wait_for', + refresh, body: bulkUpdateParams, }) : {}; @@ -794,7 +815,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, namespace } = options; + const { migrationVersion, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); @@ -811,7 +832,7 @@ export class SavedObjectsRepository { const response = await this._writeToCluster('update', { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), - refresh: 'wait_for', + refresh, _source: true, body: { script: { diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 63c9a0ee35ae0..c6de9fa94291c 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -33,4 +33,4 @@ const create = () => update: jest.fn(), } as unknown) as jest.Mocked); -export const SavedObjectsClientMock = { create }; +export const savedObjectsClientMock = { create }; diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 4e04a08bd5212..550e8a1de0d80 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -24,6 +24,7 @@ import { SavedObjectReference, SavedObjectsMigrationVersion, SavedObjectsBaseOptions, + MutatingOperationRefreshSetting, SavedObjectsFindOptions, } from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; @@ -40,6 +41,8 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; } /** @@ -101,6 +104,35 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { version?: string; /** {@inheritdoc SavedObjectReference} */ references?: SavedObjectReference[]; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * + * @public + */ +export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * + * @public + */ +export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * + * @public + */ +export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; } /** @@ -189,7 +221,7 @@ export class SavedObjectsClient { * @param id * @param options */ - async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { + async delete(type: string, id: string, options: SavedObjectsDeleteOptions = {}) { return await this._repository.delete(type, id, options); } @@ -260,7 +292,7 @@ export class SavedObjectsClient { */ async bulkUpdate( objects: Array>, - options?: SavedObjectsBaseOptions + options?: SavedObjectsBulkUpdateOptions ): Promise> { return await this._repository.bulkUpdate(objects, options); } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a968b6d9392f8..2c6f5e4a520a7 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -142,6 +142,12 @@ export interface SavedObjectsBaseOptions { namespace?: string; } +/** + * Elasticsearch Refresh setting for mutating operation + * @public + */ +export type MutatingOperationRefreshSetting = boolean | 'wait_for'; + /** * Saved Objects is Kibana's data persisentence mechanism allowing plugins to * use Elasticsearch for storing plugin state. diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9740f1f7032d1..e7a4cdc0174b0 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -942,6 +942,9 @@ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; // @public (undocumented) export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; +// @public +export type MutatingOperationRefreshSetting = boolean | 'wait_for'; + // Warning: (ae-forgotten-export) The symbol "OnPostAuthResult" needs to be exported by the entry point index.d.ts // // @public @@ -1196,6 +1199,11 @@ export interface SavedObjectsBulkUpdateObject { // (undocumented) @@ -1208,9 +1216,9 @@ export class SavedObjectsClient { constructor(repository: SavedObjectsRepository); bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; - bulkUpdate(objects: Array>, options?: SavedObjectsBaseOptions): Promise>; + bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - delete(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<{}>; + delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; // (undocumented) errors: typeof SavedObjectsErrorHelpers; // (undocumented) @@ -1247,6 +1255,12 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { overwrite?: boolean; // (undocumented) references?: SavedObjectReference[]; + refresh?: MutatingOperationRefreshSetting; +} + +// @public (undocumented) +export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; } // @public (undocumented) @@ -1554,6 +1568,7 @@ export class SavedObjectsSerializer { // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; + refresh?: MutatingOperationRefreshSetting; version?: string; } diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index 832d61bdb4137..e219d0c962344 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -23,7 +23,7 @@ import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock import { UiSettingsService } from './ui_settings_service'; import { httpServiceMock } from '../http/http_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { SavedObjectsClientMock } from '../mocks'; +import { savedObjectsClientMock } from '../mocks'; import { mockCoreContext } from '../core_context.mock'; const overrides = { @@ -43,7 +43,7 @@ const coreContext = mockCoreContext.create(); coreContext.configService.atPath.mockReturnValue(new BehaviorSubject({ overrides })); const httpSetup = httpServiceMock.createSetupContract(); const setupDeps = { http: httpSetup }; -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); afterEach(() => { MockUiSettingsClientConstructor.mockClear(); diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 3b239bd3ff731..805b77365e624 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -2,6 +2,10 @@ set -e +if [[ "$CI_ENV_SETUP" ]]; then + return 0 +fi + installNode=$1 dir="$(pwd)" @@ -152,3 +156,5 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then echo "Setting JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA" export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi + +export CI_ENV_SETUP=true diff --git a/src/dev/i18n/extractors/code.js b/src/dev/i18n/extractors/code.js index 58ca059be5491..fa0d834824e97 100644 --- a/src/dev/i18n/extractors/code.js +++ b/src/dev/i18n/extractors/code.js @@ -67,7 +67,7 @@ export function* extractCodeMessages(buffer, reporter) { try { ast = parse(buffer.toString(), { sourceType: 'module', - plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators'], + plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators', 'dynamicImport'], }); } catch (error) { if (error instanceof SyntaxError) { diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index 727c51f704431..fbd16d95ded1c 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -106,8 +106,5 @@ export const LICENSE_OVERRIDES = { // TODO can be removed once we upgrade the use of walk dependency past or equal to v2.3.14 'walk@2.3.9': ['MIT'], - // TODO remove this once we upgrade past or equal to v1.0.2 - 'babel-plugin-mock-imports@1.0.1': ['MIT'], - '@elastic/node-ctags@1.0.2': ['Nuclide software'], }; diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 9829de2fd3920..edd818e1b42de 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -43,8 +43,9 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/docs/**/*', 'src/legacy/ui/public/assets/fonts/**/*', 'packages/kbn-utility-types/test-d/**/*', - 'Jenkinsfile', + '**/Jenkinsfile*', 'Dockerfile*', + 'vars/*', // Files in this directory must match a pre-determined name in some cases. 'x-pack/legacy/plugins/canvas/.storybook/*', diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index bf6e8711fa81e..3deebcc0c18f9 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -88,7 +88,7 @@ export function runTypeCheckCli() { } execInProjects(log, projects, process.execPath, project => [ - ...(project.name === 'x-pack' ? ['--max-old-space-size=2048'] : []), + ...(project.name === 'x-pack' ? ['--max-old-space-size=4096'] : []), require.resolve('typescript/bin/tsc'), ...['--project', project.tsConfigPath], ...tscArgs, diff --git a/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.test.ts b/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.test.ts new file mode 100644 index 0000000000000..0dc1bcc96ddee --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.test.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Range as AceRange } from 'brace'; +import { LegacyEditor } from './legacy_editor'; + +describe('Legacy Editor', () => { + const aceMock: any = { + getValue() { + return 'ok'; + }, + + getCursorPosition() { + return { + row: 1, + column: 1, + }; + }, + + getSession() { + return { + replace(range: AceRange, value: string) {}, + getLine(n: number) { + return 'line'; + }, + doc: { + getTextRange(r: any) { + return ''; + }, + }, + getState(n: number) { + return n; + }, + }; + }, + }; + + // This is to ensure that we are correctly importing Ace's Range component + it('smoke tests for updates to ranges', () => { + const legacyEditor = new LegacyEditor(aceMock); + legacyEditor.getValueInRange({ + start: { lineNumber: 1, column: 1 }, + end: { lineNumber: 2, column: 2 }, + }); + legacyEditor.replace( + { + start: { lineNumber: 1, column: 1 }, + end: { lineNumber: 2, column: 2 }, + }, + 'test!' + ); + }); +}); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.ts index 1b083adcfea76..f8c3f425a1032 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/models/legacy_editor.ts @@ -17,10 +17,13 @@ * under the License. */ -import { Editor as IAceEditor, Range as AceRange } from 'brace'; +import ace from 'brace'; +import { Editor as IAceEditor } from 'brace'; import { CoreEditor, Position, Range, Token, TokensProvider } from '../../types'; import { AceTokensProvider } from '../../lib/ace_token_provider'; +const _AceRange = ace.acequire('ace/range').Range; + export class LegacyEditor implements CoreEditor { constructor(private readonly editor: IAceEditor) {} @@ -31,7 +34,7 @@ export class LegacyEditor implements CoreEditor { getValueInRange({ start, end }: Range): string { const session = this.editor.getSession(); - const aceRange = new AceRange( + const aceRange = new _AceRange( start.lineNumber - 1, start.column - 1, end.lineNumber - 1, @@ -90,7 +93,7 @@ export class LegacyEditor implements CoreEditor { } replace({ start, end }: Range, value: string): void { - const aceRange = new AceRange( + const aceRange = new _AceRange( start.lineNumber - 1, start.column - 1, end.lineNumber - 1, diff --git a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete.ts b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete.ts index 41869f41c222f..47edf42f0eec5 100644 --- a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete.ts +++ b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete.ts @@ -317,16 +317,16 @@ export default function({ coreEditor: editor, parser, execCommand, - getCursor, - isCompleteActive, + getCursorPosition, + isCompleterActive, addChangeListener, removeChangeListener, }: { coreEditor: LegacyEditor; parser: any; execCommand: (cmd: string) => void; - getCursor: () => any; - isCompleteActive: () => boolean; + getCursorPosition: () => Position | null; + isCompleterActive: () => boolean; addChangeListener: (fn: any) => void; removeChangeListener: (fn: any) => void; }) { @@ -969,11 +969,10 @@ export default function({ 100); function editorChangeListener() { - const cursor = getCursor(); - if (isCompleteActive()) { - return; + const position = getCursorPosition(); + if (position && !isCompleterActive()) { + evaluateCurrentTokenAfterAChange(position); } - evaluateCurrentTokenAfterAChange(cursor); } function getCompletions( diff --git a/src/legacy/core_plugins/console/public/quarantined/src/input.ts b/src/legacy/core_plugins/console/public/quarantined/src/input.ts index a5e38e7a06e0e..eb93f8e165cb5 100644 --- a/src/legacy/core_plugins/console/public/quarantined/src/input.ts +++ b/src/legacy/core_plugins/console/public/quarantined/src/input.ts @@ -24,6 +24,7 @@ import { LegacyEditor } from '../../../np_ready/public/application/models'; // @ts-ignore import SenseEditor from './sense_editor/editor'; +import { Position } from '../../../np_ready/public/types'; let input: any; export function initializeEditor($el: JQuery, $actionsEl: JQuery) { @@ -35,8 +36,18 @@ export function initializeEditor($el: JQuery, $actionsEl: JQuery input.execCommand(cmd), - getCursor: () => input.selection.lead, - isCompleteActive: () => input.__ace.completer && input.__ace.completer.activated, + getCursorPosition: (): Position | null => { + if (input.selection && input.selection.lead) { + return { + lineNumber: input.selection.lead.row + 1, + column: input.selection.lead.column + 1, + }; + } + return null; + }, + isCompleterActive: () => { + return Boolean(input.__ace.completer && input.__ace.completer.activated); + }, addChangeListener: (fn: any) => input.on('changeSelection', fn), removeChangeListener: (fn: any) => input.off('changeSelection', fn), }; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap index f59afc7165bab..286e60cca9712 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap @@ -103,6 +103,13 @@ exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAuto }, "chrome": Object { "addApplicationClass": [MockFunction], + "docTitle": Object { + "__legacy": Object { + "setBaseTitle": [MockFunction], + }, + "change": [MockFunction], + "reset": [MockFunction], + }, "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBrand$": [MockFunction], @@ -659,6 +666,13 @@ exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAuto }, "chrome": Object { "addApplicationClass": [MockFunction], + "docTitle": Object { + "__legacy": Object { + "setBaseTitle": [MockFunction], + }, + "change": [MockFunction], + "reset": [MockFunction], + }, "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBrand$": [MockFunction], @@ -1203,6 +1217,13 @@ exports[`QueryBarInput Should pass the query language to the language switcher 1 }, "chrome": Object { "addApplicationClass": [MockFunction], + "docTitle": Object { + "__legacy": Object { + "setBaseTitle": [MockFunction], + }, + "change": [MockFunction], + "reset": [MockFunction], + }, "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBrand$": [MockFunction], @@ -1756,6 +1777,13 @@ exports[`QueryBarInput Should pass the query language to the language switcher 1 }, "chrome": Object { "addApplicationClass": [MockFunction], + "docTitle": Object { + "__legacy": Object { + "setBaseTitle": [MockFunction], + }, + "change": [MockFunction], + "reset": [MockFunction], + }, "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBrand$": [MockFunction], @@ -2300,6 +2328,13 @@ exports[`QueryBarInput Should render the given query 1`] = ` }, "chrome": Object { "addApplicationClass": [MockFunction], + "docTitle": Object { + "__legacy": Object { + "setBaseTitle": [MockFunction], + }, + "change": [MockFunction], + "reset": [MockFunction], + }, "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBrand$": [MockFunction], @@ -2853,6 +2888,13 @@ exports[`QueryBarInput Should render the given query 1`] = ` }, "chrome": Object { "addApplicationClass": [MockFunction], + "docTitle": Object { + "__legacy": Object { + "setBaseTitle": [MockFunction], + }, + "change": [MockFunction], + "reset": [MockFunction], + }, "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBrand$": [MockFunction], diff --git a/src/legacy/core_plugins/expressions/index.ts b/src/legacy/core_plugins/expressions/index.ts index b10e9a8dd5442..4ba9a74795d4c 100644 --- a/src/legacy/core_plugins/expressions/index.ts +++ b/src/legacy/core_plugins/expressions/index.ts @@ -34,6 +34,7 @@ export default function DataExpressionsPlugin(kibana: any) { init: (server: Legacy.Server) => ({}), uiExports: { injectDefaultVars: () => ({}), + styleSheetPaths: resolve(__dirname, 'public/index.scss'), }, }; diff --git a/src/legacy/core_plugins/expressions/public/index.scss b/src/legacy/core_plugins/expressions/public/index.scss new file mode 100644 index 0000000000000..6efc552bf319b --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/index.scss @@ -0,0 +1,13 @@ +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +/* Expressions plugin styles */ + +// Prefix all styles with "exp" to avoid conflicts. +// Examples +// expChart +// expChart__legend +// expChart__legend--small +// expChart__legend-isLoading + +@import './np_ready/public/index'; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/_expression_renderer.scss b/src/legacy/core_plugins/expressions/public/np_ready/public/_expression_renderer.scss new file mode 100644 index 0000000000000..4f030384ed883 --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/_expression_renderer.scss @@ -0,0 +1,20 @@ +.expExpressionRenderer { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.expExpressionRenderer__expression { + width: 100%; + height: 100%; +} + +.expExpressionRenderer-isEmpty, +.expExpressionRenderer-hasError { + .expExpressionRenderer__expression { + display: none; + } +} diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/_index.scss b/src/legacy/core_plugins/expressions/public/np_ready/public/_index.scss new file mode 100644 index 0000000000000..b9df491cd6e79 --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/_index.scss @@ -0,0 +1 @@ +@import './expression_renderer'; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts index b4b11588b91bf..8043e0fb6e3f9 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts @@ -61,6 +61,7 @@ export class ExpressionDataHandler { this.promise = interpreter.interpretAst(this.ast, params.context || defaultContext, { getInitialContext, inspectorAdapters: this.inspectorAdapters, + abortSignal: this.abortController.signal, }); } @@ -69,7 +70,18 @@ export class ExpressionDataHandler { }; getData = async () => { - return await this.promise; + try { + return await this.promise; + } catch (e) { + return { + type: 'error', + error: { + type: e.type, + message: e.message, + stack: e.stack, + }, + }; + } }; getExpression = () => { diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.test.tsx b/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.test.tsx new file mode 100644 index 0000000000000..26db8753e6403 --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.test.tsx @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Subject } from 'rxjs'; +import { share } from 'rxjs/operators'; +import { ExpressionRendererImplementation } from './expression_renderer'; +import { ExpressionLoader } from './loader'; +import { mount } from 'enzyme'; +import { EuiProgress } from '@elastic/eui'; + +jest.mock('./loader', () => { + return { + ExpressionLoader: jest.fn().mockImplementation(() => { + return {}; + }), + loader: jest.fn(), + }; +}); + +describe('ExpressionRenderer', () => { + it('starts to load, resolves, and goes back to loading', () => { + const dataSubject = new Subject(); + const data$ = dataSubject.asObservable().pipe(share()); + const renderSubject = new Subject(); + const render$ = renderSubject.asObservable().pipe(share()); + const loadingSubject = new Subject(); + const loading$ = loadingSubject.asObservable().pipe(share()); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$, + data$, + loading$, + update: jest.fn(), + }; + }); + + const instance = mount(); + + loadingSubject.next(); + instance.update(); + + expect(instance.find(EuiProgress)).toHaveLength(1); + + renderSubject.next(1); + + instance.update(); + + expect(instance.find(EuiProgress)).toHaveLength(0); + + instance.setProps({ expression: 'something new' }); + loadingSubject.next(); + instance.update(); + + expect(instance.find(EuiProgress)).toHaveLength(1); + + renderSubject.next(1); + instance.update(); + + expect(instance.find(EuiProgress)).toHaveLength(0); + }); + + it('should display an error message when the expression fails', () => { + const dataSubject = new Subject(); + const data$ = dataSubject.asObservable().pipe(share()); + const renderSubject = new Subject(); + const render$ = renderSubject.asObservable().pipe(share()); + const loadingSubject = new Subject(); + const loading$ = loadingSubject.asObservable().pipe(share()); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$, + data$, + loading$, + update: jest.fn(), + }; + }); + + const instance = mount(); + + dataSubject.next('good data'); + renderSubject.next({ + type: 'error', + error: { message: 'render error' }, + }); + instance.update(); + + expect(instance.find(EuiProgress)).toHaveLength(0); + expect(instance.find('[data-test-subj="expression-renderer-error"]')).toHaveLength(1); + }); + + it('should display a custom error message if the user provides one', () => { + const dataSubject = new Subject(); + const data$ = dataSubject.asObservable().pipe(share()); + const renderSubject = new Subject(); + const render$ = renderSubject.asObservable().pipe(share()); + const loadingSubject = new Subject(); + const loading$ = loadingSubject.asObservable().pipe(share()); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$, + data$, + loading$, + update: jest.fn(), + }; + }); + + const renderErrorFn = jest.fn().mockReturnValue(null); + + const instance = mount( + + ); + + renderSubject.next({ + type: 'error', + error: { message: 'render error' }, + }); + instance.update(); + + expect(renderErrorFn).toHaveBeenCalledWith('render error'); + }); +}); diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx b/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx index 9edb5f098ba2e..8359663610f29 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx @@ -17,49 +17,49 @@ * under the License. */ -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, useState } from 'react'; import React from 'react'; -import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './types'; -import { IExpressionLoader, ExpressionLoader } from './loader'; +import classNames from 'classnames'; +import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; +import { ExpressionAST, IExpressionLoaderParams, IInterpreterErrorResult } from './types'; +import { ExpressionLoader } from './loader'; // Accept all options of the runner as props except for the // dom element which is provided by the component itself export interface ExpressionRendererProps extends IExpressionLoaderParams { - className: string; dataAttrs?: string[]; expression: string | ExpressionAST; - /** - * If an element is specified, but the response of the expression run can't be rendered - * because it isn't a valid response or the specified renderer isn't available, - * this callback is called with the given result. - */ - onRenderFailure?: (result: IInterpreterResult) => void; + renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; +} + +interface State { + isEmpty: boolean; + isLoading: boolean; + error: null | Error; } export type ExpressionRenderer = React.FC; -export const createRenderer = (loader: IExpressionLoader): ExpressionRenderer => ({ - className, +const defaultState: State = { + isEmpty: true, + isLoading: false, + error: null, +}; + +export const ExpressionRendererImplementation = ({ dataAttrs, expression, - onRenderFailure, + renderError, ...options }: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); const handlerRef: React.MutableRefObject = useRef(null); + const [state, setState] = useState({ ...defaultState }); + // Re-fetch data automatically when the inputs change useEffect(() => { - if (mountpoint.current) { - if (!handlerRef.current) { - handlerRef.current = loader(mountpoint.current, expression, options); - } else { - handlerRef.current.update(expression, options); - } - handlerRef.current.data$.toPromise().catch(result => { - if (onRenderFailure) { - onRenderFailure(result); - } - }); + if (handlerRef.current) { + handlerRef.current.update(expression, options); } }, [ expression, @@ -67,8 +67,66 @@ export const createRenderer = (loader: IExpressionLoader): ExpressionRenderer => options.context, options.variables, options.disableCaching, - mountpoint.current, ]); - return
; + // Initialize the loader only once + useEffect(() => { + if (mountpoint.current && !handlerRef.current) { + handlerRef.current = new ExpressionLoader(mountpoint.current, expression, options); + + handlerRef.current.loading$.subscribe(() => { + if (!handlerRef.current) { + return; + } + setState(prevState => ({ ...prevState, isLoading: true })); + }); + handlerRef.current.render$.subscribe((item: number | IInterpreterErrorResult) => { + if (!handlerRef.current) { + return; + } + if (typeof item !== 'number') { + setState(() => ({ + ...defaultState, + isEmpty: false, + error: item.error, + })); + } else { + setState(() => ({ + ...defaultState, + isEmpty: false, + })); + } + }); + } + }, [mountpoint.current]); + + useEffect(() => { + // We only want a clean up to run when the entire component is unloaded, not on every render + return function cleanup() { + if (handlerRef.current) { + handlerRef.current.destroy(); + handlerRef.current = null; + } + }; + }, []); + + const classes = classNames('expExpressionRenderer', { + 'expExpressionRenderer-isEmpty': state.isEmpty, + 'expExpressionRenderer-hasError': !!state.error, + }); + + return ( +
+ {state.isEmpty ? : null} + {state.isLoading ? : null} + {!state.isLoading && state.error ? ( + renderError ? ( + renderError(state.error.message) + ) : ( +
{state.error.message}
+ ) + ) : null} +
+
+ ); }; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts index 428b431d298ad..2ff71a6df60cf 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts @@ -22,7 +22,7 @@ import { ExpressionsPublicPlugin } from './plugin'; export * from './plugin'; export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; -export { IInterpreterRenderFunction } from './types'; +export { IInterpreterRenderFunction, IInterpreterRenderHandlers } from './types'; export function plugin(initializerContext: PluginInitializerContext) { return new ExpressionsPublicPlugin(initializerContext); diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts index df695c039e02e..36917bf4e8384 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts @@ -19,6 +19,7 @@ import { first } from 'rxjs/operators'; import { loader, ExpressionLoader } from './loader'; +import { ExpressionDataHandler } from './execute'; import { fromExpression } from '@kbn/interpreter/common'; import { IInterpreterRenderHandlers } from './types'; import { Observable } from 'rxjs'; @@ -48,27 +49,37 @@ jest.mock('./services', () => { }; }); +jest.mock('./execute', () => { + const actual = jest.requireActual('./execute'); + return { + ExpressionDataHandler: jest + .fn() + .mockImplementation((...args) => new actual.ExpressionDataHandler(...args)), + execute: jest.fn().mockReturnValue(actual.execute), + }; +}); + describe('execute helper function', () => { - it('returns ExpressionDataHandler instance', () => { + it('returns ExpressionLoader instance', () => { const response = loader(element, '', {}); expect(response).toBeInstanceOf(ExpressionLoader); }); }); -describe('ExpressionDataHandler', () => { +describe('ExpressionLoader', () => { const expressionString = ''; describe('constructor', () => { it('accepts expression string', () => { - const expressionDataHandler = new ExpressionLoader(element, expressionString, {}); - expect(expressionDataHandler.getExpression()).toEqual(expressionString); + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + expect(expressionLoader.getExpression()).toEqual(expressionString); }); it('accepts expression AST', () => { const expressionAST = fromExpression(expressionString) as ExpressionAST; - const expressionDataHandler = new ExpressionLoader(element, expressionAST, {}); - expect(expressionDataHandler.getExpression()).toEqual(expressionString); - expect(expressionDataHandler.getAst()).toEqual(expressionAST); + const expressionLoader = new ExpressionLoader(element, expressionAST, {}); + expect(expressionLoader.getExpression()).toEqual(expressionString); + expect(expressionLoader.getAst()).toEqual(expressionAST); }); it('creates observables', () => { @@ -117,9 +128,86 @@ describe('ExpressionDataHandler', () => { expect(response).toBe(2); }); - it('cancel() aborts request', () => { - const expressionDataHandler = new ExpressionLoader(element, expressionString, {}); - expressionDataHandler.cancel(); + it('cancels the previous request when the expression is updated', () => { + const cancelMock = jest.fn(); + + (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ + getData: () => true, + cancel: cancelMock, + })); + + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + expressionLoader.update('new', {}); + + expect(cancelMock).toHaveBeenCalledTimes(1); + }); + + it('does not send an observable message if a request was aborted', () => { + const cancelMock = jest.fn(); + + const getData = jest + .fn() + .mockResolvedValueOnce({ + type: 'error', + error: { + name: 'AbortError', + }, + }) + .mockResolvedValueOnce({ + type: 'real', + }); + + (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ + getData, + cancel: cancelMock, + })); + (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ + getData, + cancel: cancelMock, + })); + + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + + expect.assertions(2); + expressionLoader.data$.subscribe({ + next(data) { + expect(data).toEqual({ + type: 'real', + }); + }, + error() { + expect(false).toEqual('Should not be called'); + }, + }); + + expressionLoader.update('new expression', {}); + + expect(getData).toHaveBeenCalledTimes(2); + }); + + it('sends an observable error if the data fetching failed', () => { + const cancelMock = jest.fn(); + + const getData = jest.fn().mockResolvedValue('rejected'); + + (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ + getData, + cancel: cancelMock, + })); + + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + + expect.assertions(2); + expressionLoader.data$.subscribe({ + next(data) { + expect(data).toEqual('Should not be called'); + }, + error(error) { + expect(error.message).toEqual('Could not fetch data'); + }, + }); + + expect(getData).toHaveBeenCalledTimes(1); }); it('inspect() returns correct inspector adapters', () => { diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts index b07b0048bfd52..709fbc78a9b52 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts @@ -18,11 +18,11 @@ */ import { Observable, Subject } from 'rxjs'; -import { first, share } from 'rxjs/operators'; +import { share } from 'rxjs/operators'; import { Adapters, InspectorSession } from '../../../../../../plugins/inspector/public'; -import { execute, ExpressionDataHandler } from './execute'; +import { ExpressionDataHandler } from './execute'; import { ExpressionRenderHandler } from './render'; -import { RenderId, Data, IExpressionLoaderParams, ExpressionAST } from './types'; +import { Data, IExpressionLoaderParams, ExpressionAST } from './types'; import { getInspector } from './services'; export class ExpressionLoader { @@ -32,12 +32,12 @@ export class ExpressionLoader { events$: ExpressionRenderHandler['events$']; loading$: Observable; - private dataHandler!: ExpressionDataHandler; + private dataHandler: ExpressionDataHandler | undefined; private renderHandler: ExpressionRenderHandler; private dataSubject: Subject; private loadingSubject: Subject; private data: Data; - private params: IExpressionLoaderParams; + private params: IExpressionLoaderParams = {}; constructor( element: HTMLElement, @@ -63,76 +63,108 @@ export class ExpressionLoader { this.render(data); }); - this.params = { - searchContext: { type: 'kibana_context' }, - extraHandlers: params.extraHandlers, - }; + this.setParams(params); - this.execute(expression, params); + this.loadData(expression, this.params); } - destroy() {} + destroy() { + this.dataSubject.complete(); + this.loadingSubject.complete(); + this.renderHandler.destroy(); + if (this.dataHandler) { + this.dataHandler.cancel(); + } + } cancel() { - this.dataHandler.cancel(); + if (this.dataHandler) { + this.dataHandler.cancel(); + } } - getExpression(): string { - return this.dataHandler.getExpression(); + getExpression(): string | undefined { + if (this.dataHandler) { + return this.dataHandler.getExpression(); + } } - getAst(): ExpressionAST { - return this.dataHandler.getAst(); + getAst(): ExpressionAST | undefined { + if (this.dataHandler) { + return this.dataHandler.getAst(); + } } getElement(): HTMLElement { return this.renderHandler.getElement(); } - openInspector(title: string): InspectorSession { - return getInspector().open(this.inspect(), { - title, - }); + openInspector(title: string): InspectorSession | undefined { + const inspector = this.inspect(); + if (inspector) { + return getInspector().open(inspector, { + title, + }); + } } - inspect(): Adapters { - return this.dataHandler.inspect(); + inspect(): Adapters | undefined { + if (this.dataHandler) { + return this.dataHandler.inspect(); + } } - update(expression?: string | ExpressionAST, params?: IExpressionLoaderParams): Promise { - const promise = this.render$.pipe(first()).toPromise(); - if (params && params.searchContext && this.params.searchContext) { - this.params.searchContext = _.defaults( - {}, - params.searchContext, - this.params.searchContext - ) as any; - } + update(expression?: string | ExpressionAST, params?: IExpressionLoaderParams): void { + this.setParams(params); - this.loadingSubject.next(); if (expression) { - this.execute(expression, this.params); + this.loadData(expression, this.params); } else { this.render(this.data); } - return promise; } - private execute = async ( + private loadData = async ( expression: string | ExpressionAST, - params?: IExpressionLoaderParams - ): Promise => { + params: IExpressionLoaderParams + ): Promise => { + this.loadingSubject.next(); if (this.dataHandler) { this.dataHandler.cancel(); } - this.dataHandler = execute(expression, params); - this.data = await this.dataHandler.getData(); - this.dataSubject.next(this.data); - return this.data; + this.setParams(params); + this.dataHandler = new ExpressionDataHandler(expression, params); + const data = await this.dataHandler.getData(); + this.dataSubject.next(data); }; - private async render(data: Data): Promise { - return this.renderHandler.render(data, this.params.extraHandlers); + private render(data: Data): void { + this.loadingSubject.next(); + this.renderHandler.render(data, this.params.extraHandlers); + } + + private setParams(params?: IExpressionLoaderParams) { + if (!params || !Object.keys(params).length) { + return; + } + + if (params.searchContext && this.params.searchContext) { + this.params.searchContext = _.defaults( + {}, + params.searchContext, + this.params.searchContext + ) as any; + } + if (params.extraHandlers && this.params) { + this.params.extraHandlers = params.extraHandlers; + } + + if (!Object.keys(this.params).length) { + this.params = { + ...params, + searchContext: { type: 'kibana_context', ...(params.searchContext || {}) }, + }; + } } } diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.tsx similarity index 97% rename from src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/mocks.tsx index 6569e8d8d1ec5..1a2f473f4c9a1 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.tsx @@ -17,6 +17,7 @@ * under the License. */ +import React from 'react'; import { ExpressionsSetup, ExpressionsStart, plugin as pluginInitializer } from '.'; /* eslint-disable */ import { coreMock } from '../../../../../../core/public/mocks'; @@ -44,7 +45,7 @@ const createExpressionsSetupMock = (): ExpressionsSetup => { function createExpressionsStartMock(): ExpressionsStart { return { - ExpressionRenderer: jest.fn(() => null), + ExpressionRenderer: jest.fn(props => <>), execute: jest.fn(), loader: jest.fn(), render: jest.fn(), diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts index 5a0eecc51ef19..ec70248694990 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts @@ -34,7 +34,7 @@ import { } from '../../../../../../plugins/inspector/public'; import { IInterpreter } from './types'; import { setInterpreter, setInspector, setRenderersRegistry } from './services'; -import { createRenderer } from './expression_renderer'; +import { ExpressionRendererImplementation } from './expression_renderer'; import { ExpressionLoader, loader } from './loader'; import { ExpressionDataHandler, execute } from './execute'; import { ExpressionRenderHandler, render } from './render'; @@ -77,17 +77,16 @@ export class ExpressionsPublicPlugin } public start(core: CoreStart, { inspector }: ExpressionsStartDeps) { - const ExpressionRenderer = createRenderer(loader); setInspector(inspector); return { execute, render, loader, + ExpressionRenderer: ExpressionRendererImplementation, ExpressionDataHandler, ExpressionRenderHandler, ExpressionLoader, - ExpressionRenderer, }; } diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts index cf606b8fabec2..9d555f9760ee7 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts @@ -20,6 +20,8 @@ import { render, ExpressionRenderHandler } from './render'; import { Observable } from 'rxjs'; import { IInterpreterRenderHandlers } from './types'; +import { getRenderersRegistry } from './services'; +import { first } from 'rxjs/operators'; const element: HTMLElement = {} as HTMLElement; @@ -31,10 +33,11 @@ jest.mock('./services', () => { }, }, }; + return { - getRenderersRegistry: () => ({ - get: (id: string) => renderers[id], - }), + getRenderersRegistry: jest.fn(() => ({ + get: jest.fn((id: string) => renderers[id]), + })), }; }); @@ -46,8 +49,6 @@ describe('render helper function', () => { }); describe('ExpressionRenderHandler', () => { - const data = { type: 'render', as: 'test' }; - it('constructor creates observers', () => { const expressionRenderHandler = new ExpressionRenderHandler(element); expect(expressionRenderHandler.events$).toBeInstanceOf(Observable); @@ -61,27 +62,71 @@ describe('ExpressionRenderHandler', () => { }); describe('render()', () => { - it('throws if invalid data is provided', async () => { + it('sends an observable error and keeps it open if invalid data is provided', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); - await expect(expressionRenderHandler.render({})).rejects.toThrow(); + const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise(); + expressionRenderHandler.render(false); + await expect(promise1).resolves.toEqual({ + type: 'error', + error: { + message: 'invalid data provided to the expression renderer', + }, + }); + + const promise2 = expressionRenderHandler.render$.pipe(first()).toPromise(); + expressionRenderHandler.render(false); + await expect(promise2).resolves.toEqual({ + type: 'error', + error: { + message: 'invalid data provided to the expression renderer', + }, + }); }); - it('throws if renderer does not exist', async () => { + it('sends an observable error if renderer does not exist', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); - await expect( - expressionRenderHandler.render({ type: 'render', as: 'something' }) - ).rejects.toThrow(); + const promise = expressionRenderHandler.render$.pipe(first()).toPromise(); + expressionRenderHandler.render({ type: 'render', as: 'something' }); + await expect(promise).resolves.toEqual({ + type: 'error', + error: { + message: `invalid renderer id 'something'`, + }, + }); }); - it('returns a promise', () => { + it('sends an observable error if the rendering function throws', async () => { + (getRenderersRegistry as jest.Mock).mockReturnValueOnce({ get: () => true }); + (getRenderersRegistry as jest.Mock).mockReturnValueOnce({ + get: () => ({ + render: () => { + throw new Error('renderer error'); + }, + }), + }); + const expressionRenderHandler = new ExpressionRenderHandler(element); - expect(expressionRenderHandler.render(data)).toBeInstanceOf(Promise); + const promise = expressionRenderHandler.render$.pipe(first()).toPromise(); + expressionRenderHandler.render({ type: 'render', as: 'something' }); + await expect(promise).resolves.toEqual({ + type: 'error', + error: { + message: 'renderer error', + }, + }); }); - it('resolves a promise once rendering is complete', async () => { + it('sends a next observable once rendering is complete', () => { const expressionRenderHandler = new ExpressionRenderHandler(element); - const response = await expressionRenderHandler.render(data); - expect(response).toBe(1); + expect.assertions(1); + return new Promise(resolve => { + expressionRenderHandler.render$.subscribe(renderCount => { + expect(renderCount).toBe(1); + resolve(); + }); + + expressionRenderHandler.render({ type: 'render', as: 'test' }); + }); }); }); }); diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/render.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/render.ts index 96f56a4b36202..f67b4c2d8e272 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/render.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/render.ts @@ -19,7 +19,7 @@ import { Observable } from 'rxjs'; import * as Rx from 'rxjs'; -import { share, first } from 'rxjs/operators'; +import { share } from 'rxjs/operators'; import { event, RenderId, Data, IInterpreterRenderHandlers } from './types'; import { getRenderersRegistry } from './services'; @@ -33,19 +33,22 @@ export class ExpressionRenderHandler { private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; + private renderSubject: Rx.Subject; + private eventsSubject: Rx.Subject; + private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; constructor(element: HTMLElement) { this.element = element; - const eventsSubject = new Rx.Subject(); - this.events$ = eventsSubject.asObservable().pipe(share()); + this.eventsSubject = new Rx.Subject(); + this.events$ = this.eventsSubject.asObservable().pipe(share()); - const renderSubject = new Rx.Subject(); - this.render$ = renderSubject.asObservable().pipe(share()); + this.renderSubject = new Rx.Subject(); + this.render$ = this.renderSubject.asObservable().pipe(share()); - const updateSubject = new Rx.Subject(); - this.update$ = updateSubject.asObservable().pipe(share()); + this.updateSubject = new Rx.Subject(); + this.update$ = this.updateSubject.asObservable().pipe(share()); this.handlers = { onDestroy: (fn: any) => { @@ -53,39 +56,68 @@ export class ExpressionRenderHandler { }, done: () => { this.renderCount++; - renderSubject.next(this.renderCount); + this.renderSubject.next(this.renderCount); }, reload: () => { - updateSubject.next(null); + this.updateSubject.next(null); }, update: params => { - updateSubject.next(params); + this.updateSubject.next(params); }, event: data => { - eventsSubject.next(data); + this.eventsSubject.next(data); }, }; } - render = async (data: Data, extraHandlers: IExpressionRendererExtraHandlers = {}) => { - if (!data || data.type !== 'render' || !data.as) { - throw new Error('invalid data provided to expression renderer'); + render = (data: Data, extraHandlers: IExpressionRendererExtraHandlers = {}) => { + if (!data || typeof data !== 'object') { + this.renderSubject.next({ + type: 'error', + error: { + message: 'invalid data provided to the expression renderer', + }, + }); + return; } - if (!getRenderersRegistry().get(data.as)) { - throw new Error(`invalid renderer id '${data.as}'`); + if (data.type !== 'render' || !data.as) { + if (data.type === 'error') { + this.renderSubject.next(data); + } else { + this.renderSubject.next({ + type: 'error', + error: { message: 'invalid data provided to the expression renderer' }, + }); + } + return; } - const promise = this.render$.pipe(first()).toPromise(); - - getRenderersRegistry() - .get(data.as) - .render(this.element, data.value, { ...this.handlers, ...extraHandlers }); + if (!getRenderersRegistry().get(data.as)) { + this.renderSubject.next({ + type: 'error', + error: { message: `invalid renderer id '${data.as}'` }, + }); + return; + } - return promise; + try { + // Rendering is asynchronous, completed by handlers.done() + getRenderersRegistry() + .get(data.as) + .render(this.element, data.value, { ...this.handlers, ...extraHandlers }); + } catch (e) { + this.renderSubject.next({ + type: 'error', + error: { type: e.type, message: e.message }, + }); + } }; destroy = () => { + this.renderSubject.complete(); + this.eventsSubject.complete(); + this.updateSubject.complete(); if (this.destroyFn) { this.destroyFn(); } diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/types.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/types.ts index 09aaa363c9492..0390440298c55 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/types.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/types.ts @@ -52,16 +52,25 @@ export interface IExpressionLoaderParams { export interface IInterpreterHandlers { getInitialContext: IGetInitialContext; inspectorAdapters?: Adapters; + abortSignal?: AbortSignal; } -export interface IInterpreterResult { +export interface IInterpreterErrorResult { + type: 'error'; + error: { message: string; name: string; stack: string }; +} + +export interface IInterpreterSuccessResult { type: string; as?: string; value?: unknown; error?: unknown; } +export type IInterpreterResult = IInterpreterSuccessResult & IInterpreterErrorResult; + export interface IInterpreterRenderHandlers { + // Done increments the number of rendering successes done: () => void; onDestroy: (fn: () => void) => void; reload: () => void; diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx index cfc539eafe7b9..2059e35b2c42e 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx @@ -93,11 +93,9 @@ export function DocViewTableRow({ )} {displayUnderscoreWarning && } {displayNoMappingWarning && } -
+
+ {value} +
); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/validation_wrapper.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/validation_wrapper.tsx index a3a41d2249b32..1dd1ab49d9a47 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/validation_wrapper.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/validation_wrapper.tsx @@ -29,7 +29,7 @@ interface ValidationWrapperProps extends VisOptionsProps { } interface Item { - valid: boolean; + isValid: boolean; } function ValidationWrapper({ @@ -37,20 +37,17 @@ function ValidationWrapper({ ...rest }: ValidationWrapperProps) { const [panelState, setPanelState] = useState({} as { [key: string]: Item }); - const isPanelValid = Object.values(panelState).every(item => item.valid); + const isPanelValid = Object.values(panelState).every(item => item.isValid); const { setValidity } = rest; - const setValidityHandler = useCallback( - (paramName: string, isValid: boolean) => { - setPanelState({ - ...panelState, - [paramName]: { - valid: isValid, - }, - }); - }, - [panelState] - ); + const setValidityHandler = useCallback((paramName: string, isValid: boolean) => { + setPanelState(state => ({ + ...state, + [paramName]: { + isValid, + }, + })); + }, []); useEffect(() => { setValidity(isPanelValid); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx index c1fa3475470f1..1045543512c6b 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx @@ -17,14 +17,16 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ColorSchemas } from 'ui/vislib/components/color/colormaps'; import { ColorRanges, ColorSchemaOptions, SwitchOption } from '../../common'; -import { SetColorSchemaOptionsValue } from '../../common/color_schema'; import { GaugeOptionsInternalProps } from '.'; +import { ColorSchemaVislibParams } from '../../../types'; +import { Gauge } from '../../../gauge'; function RangesPanel({ setGaugeValue, @@ -35,6 +37,22 @@ function RangesPanel({ uiState, vis, }: GaugeOptionsInternalProps) { + const setColorSchemaOptions = useCallback( + (paramName: T, value: ColorSchemaVislibParams[T]) => { + setGaugeValue(paramName, value as Gauge[T]); + // set outline if color schema is changed to greys + // if outline wasn't set explicitly yet + if ( + paramName === 'colorSchema' && + (value as string) === ColorSchemas.Greys && + typeof stateParams.gauge.outline === 'undefined' + ) { + setGaugeValue('outline', true); + } + }, + [setGaugeValue, stateParams] + ); + return ( @@ -84,7 +102,16 @@ function RangesPanel({ colorSchemas={vis.type.editorConfig.collections.colorSchemas} invertColors={stateParams.gauge.invertColors} uiState={uiState} - setValue={setGaugeValue as SetColorSchemaOptionsValue} + setValue={setColorSchemaOptions} + /> + + { describe('boundsMargin', () => { it('should set validity as true when value is positive', () => { - const comp = shallow(); - comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, 5); + defaultProps.axis.scale.boundsMargin = 5; + mount(); expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); }); it('should set validity as true when value is empty', () => { - const comp = shallow(); - comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, ''); + const comp = mount(); + comp.setProps({ + axis: { ...valueAxis, scale: { ...valueAxis.scale, boundsMargin: undefined } }, + }); expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); }); it('should set validity as false when value is negative', () => { defaultProps.axis.scale.defaultYExtents = true; - const comp = shallow(); - comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, -1); + const comp = mount(); + comp.setProps({ + axis: { ...valueAxis, scale: { ...valueAxis.scale, boundsMargin: -1 } }, + }); expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, false); }); @@ -103,7 +107,6 @@ describe('CustomExtentsOptions component', () => { const comp = shallow(); comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, false); - expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); const newScale = { ...defaultProps.axis.scale, boundsMargin: undefined, diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx index e04d8e646160e..df7eedd2c0ea1 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { ValueAxis } from '../../../types'; @@ -38,21 +38,18 @@ function CustomExtentsOptions({ setValueAxis, setValueAxisScale, }: CustomExtentsOptionsProps) { - const [isBoundsMarginValid, setIsBoundsMarginValid] = useState(true); const invalidBoundsMarginMessage = i18n.translate( 'kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin', { defaultMessage: 'Bounds margin must be greater than or equal to 0.' } ); - const setBoundsMargin = useCallback( - (paramName: 'boundsMargin', value: number | '') => { - const isValid = value === '' ? true : value >= 0; - setIsBoundsMarginValid(isValid); - setMultipleValidity('boundsMargin', isValid); + const isBoundsMarginValid = + !axis.scale.defaultYExtents || !axis.scale.boundsMargin || axis.scale.boundsMargin >= 0; - setValueAxisScale(paramName, value); - }, - [setMultipleValidity, setValueAxisScale] + const setBoundsMargin = useCallback( + (paramName: 'boundsMargin', value: number | '') => + setValueAxisScale(paramName, value === '' ? undefined : value), + [setValueAxisScale] ); const onDefaultYExtentsChange = useCallback( @@ -60,7 +57,6 @@ function CustomExtentsOptions({ const scale = { ...axis.scale, [paramName]: value }; if (!scale.defaultYExtents) { delete scale.boundsMargin; - setMultipleValidity('boundsMargin', true); } setValueAxis('scale', scale); }, @@ -79,6 +75,12 @@ function CustomExtentsOptions({ [setValueAxis, axis.scale] ); + useEffect(() => { + setMultipleValidity('boundsMargin', isBoundsMarginValid); + + return () => setMultipleValidity('boundsMargin', true); + }, [isBoundsMarginValid, setMultipleValidity]); + return ( <> setMultipleValidity('yExtents', true); - }, [isValid]); + }, [isValid, setMultipleValidity]); return ( diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.d.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.d.ts index bb042abfdcca7..ff8345e9a5b25 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.d.ts +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.d.ts @@ -32,6 +32,7 @@ interface Gauge extends ColorSchemaVislibParams { gaugeType: GaugeTypes; labels: Labels; percentageMode: boolean; + outline?: boolean; scale: { show: boolean; labels: false; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 5e8cfc8e1609c..9ac76bfcfe04e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -23,7 +23,6 @@ import _ from 'lodash'; import sinon from 'sinon'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import 'ui/private'; import '../../components/field_chooser/discover_field'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js index 3130ac29eb84d..3ddee3495f36d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js @@ -22,7 +22,6 @@ import _ from 'lodash'; import ngMock from 'ng_mock'; import { fieldCalculator } from '../../components/field_chooser/lib/field_calculator'; import expect from '@kbn/expect'; -import 'ui/private'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; // Load the kibana app dependencies. diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index c2be750ec7f63..a5b55e50eb90e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -23,7 +23,6 @@ import _ from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; import $ from 'jquery'; -import 'ui/private'; import '../../components/field_chooser/field_chooser'; import FixturesHitsProvider from 'fixtures/hits'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; diff --git a/src/legacy/core_plugins/kibana/public/discover/_index.scss b/src/legacy/core_plugins/kibana/public/discover/_index.scss index 0b0bd12cb268b..b311dd8a34778 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_index.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_index.scss @@ -11,7 +11,7 @@ @import 'components/fetch_error/index'; @import 'components/field_chooser/index'; @import 'angular/directives/index'; -@import 'doc_table/index'; +@import 'angular/doc_table/index'; @import 'hacks'; @@ -23,4 +23,4 @@ @import 'doc_viewer/index'; // Context styles -@import 'context/index'; +@import 'angular/context/index'; diff --git a/src/legacy/core_plugins/kibana/public/discover/context/index.html b/src/legacy/core_plugins/kibana/public/discover/angular/context.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/index.html rename to src/legacy/core_plugins/kibana/public/discover/angular/context.html diff --git a/src/legacy/core_plugins/kibana/public/discover/context/index.js b/src/legacy/core_plugins/kibana/public/discover/angular/context.js similarity index 87% rename from src/legacy/core_plugins/kibana/public/discover/context/index.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context.js index 902bee2badb7c..58d1626ca4b14 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context.js @@ -18,16 +18,13 @@ */ import _ from 'lodash'; - -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import uiRoutes from 'ui/routes'; import { i18n } from '@kbn/i18n'; +import { getServices, subscribeWithScope } from './../kibana_services'; -import './app'; -import contextAppRouteTemplate from './index.html'; +import './context_app'; +import contextAppRouteTemplate from './context.html'; import { getRootBreadcrumbs } from '../breadcrumbs'; -import { npStart } from 'ui/new_platform'; -import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; +const { FilterBarQueryFilterProvider, uiRoutes, chrome } = getServices(); const k7Breadcrumbs = $route => { const { indexPattern } = $route.current.locals; @@ -47,12 +44,11 @@ const k7Breadcrumbs = $route => { ]; }; - uiRoutes // deprecated route, kept for compatibility // should be removed in the future .when('/context/:indexPatternId/:type/:id*', { - redirectTo: '/context/:indexPatternId/:id' + redirectTo: '/context/:indexPatternId/:id', }) .when('/context/:indexPatternId/:id*', { controller: ContextAppRouteController, @@ -92,7 +88,7 @@ function ContextAppRouteController($routeParams, $scope, AppState, config, index }); this.anchorId = $routeParams.id; this.indexPattern = indexPattern; - this.discoverUrl = npStart.core.chrome.navLinks.get('kibana:discover').url; + this.discoverUrl = chrome.navLinks.get('kibana:discover').url; this.filters = _.cloneDeep(queryFilter.getFilters()); } diff --git a/src/legacy/core_plugins/kibana/public/discover/context/NOTES.md b/src/legacy/core_plugins/kibana/public/discover/angular/context/NOTES.md similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/NOTES.md rename to src/legacy/core_plugins/kibana/public/discover/angular/context/NOTES.md diff --git a/src/legacy/core_plugins/kibana/public/discover/context/_index.scss b/src/legacy/core_plugins/kibana/public/discover/angular/context/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/_index.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/context/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/_stubs.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js similarity index 98% rename from src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/_stubs.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js index ecb22b20e4d86..f472ff9250eb5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/_stubs.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import moment from 'moment'; -import { SearchSource } from 'ui/courier'; +import { SearchSource } from '../../../../kibana_services'; export function createIndexPatternsStub() { return { diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/anchor.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/predecessors.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/successors.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js similarity index 95% rename from src/legacy/core_plugins/kibana/public/discover/context/api/anchor.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js index 02a309eaa0165..62bbc6166662f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/api/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js @@ -18,11 +18,10 @@ */ import _ from 'lodash'; - import { i18n } from '@kbn/i18n'; +import { getServices } from '../../../kibana_services'; -import { SearchSource } from 'ui/courier'; - +const { SearchSource } = getServices(); export function fetchAnchorProvider(indexPatterns) { return async function fetchAnchor( indexPatternId, diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/discover/context/api/context.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts index 48ac59f1f0855..268f176f2c61e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts @@ -17,10 +17,8 @@ * under the License. */ -// @ts-ignore -import { SearchSource } from 'ui/courier'; import { Filter } from '@kbn/es-query'; -import { IndexPatterns, IndexPattern } from 'ui/index_patterns'; +import { IndexPatterns, IndexPattern, getServices } from '../../../kibana_services'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; @@ -36,6 +34,8 @@ export interface EsHitRecord { } export type EsHitRecordList = EsHitRecord[]; +const { SearchSource } = getServices(); + const DAY_MILLIS = 24 * 60 * 60 * 1000; // look from 1 day up to 10000 days into the past and future diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/date_conversion.test.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/date_conversion.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/date_conversion.test.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/date_conversion.test.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/sorting.test.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/sorting.test.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/date_conversion.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/date_conversion.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/date_conversion.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/date_conversion.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/fetch_hits_in_interval.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts index 9a5436b59714d..2810e5d9d7e66 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/fetch_hits_in_interval.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SearchSource } from 'ui/courier'; +import { SearchSource } from '../../../../kibana_services'; import { convertTimeValueToIso } from './date_conversion'; import { SortDirection } from './sorting'; import { EsHitRecordList } from '../context'; diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/generate_intervals.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_search_after.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_search_after.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_search_after.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_search_after.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_sort.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_sort.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/sorting.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/discover/context/api/utils/sorting.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts index b673270d7a645..4a0f531845f46 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/api/utils/sorting.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IndexPattern } from 'src/legacy/core_plugins/data/public'; +import { IndexPattern } from '../../../../kibana_services'; export enum SortDirection { asc = 'asc', diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_action_bar.scss b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/_action_bar.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_action_bar.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/_action_bar.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_index.scss b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_index.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.test.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_directive.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar_directive.ts similarity index 89% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_directive.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar_directive.ts index 0942539e63785..579d9d95c6f71 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar_directive.ts @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { wrapInI18nContext } from 'ui/i18n'; +import { getServices } from '../../../../kibana_services'; import { ActionBar } from './action_bar'; +const { uiModules, wrapInI18nContext } = getServices(); + uiModules.get('apps/context').directive('contextActionBar', function(reactDirective: any) { return reactDirective(wrapInI18nContext(ActionBar)); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_warning.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar_warning.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_warning.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/action_bar_warning.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/index.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/index.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/components/action_bar/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query/actions.js similarity index 97% rename from src/legacy/core_plugins/kibana/public/discover/context/query/actions.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query/actions.js index c55dcc374fa5a..b88e54379f448 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query/actions.js @@ -20,13 +20,13 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { toastNotifications } from 'ui/notify'; +import { toastNotifications } from '../../../kibana_services'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; import { QueryParameterActionsProvider } from '../query_parameters'; import { FAILURE_REASONS, LOADING_STATUS } from './constants'; -import { MarkdownSimple } from '../../../../../kibana_react/public'; +import { MarkdownSimple } from '../../../../../../kibana_react/public'; export function QueryActionsProvider(Private, Promise) { const fetchAnchor = Private(fetchAnchorProvider); diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query/constants.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query/constants.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query/constants.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query/constants.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query/index.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query/index.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query/index.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query/index.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query/state.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query/state.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query/state.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query/state.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/_utils.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/_utils.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/_utils.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/_utils.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js similarity index 94% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_add_filter.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js index 1c96cbeec04a3..b136b03bd500b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js @@ -20,9 +20,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import sinon from 'sinon'; - -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; - +import { getServices } from '../../../../kibana_services'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -36,7 +34,7 @@ describe('context app', function () { beforeEach(ngMock.inject(function createPrivateStubs(Private) { filterManagerStub = createQueryFilterStub(); - Private.stub(FilterBarQueryFilterProvider, filterManagerStub); + Private.stub(getServices().FilterBarQueryFilterProvider, filterManagerStub); addFilter = Private(QueryParameterActionsProvider).addFilter; })); diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_predecessor_count.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_query_parameters.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_successor_count.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js similarity index 92% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/actions.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js index 1c895b8d9e1c5..9f7b180e8fe7d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js @@ -18,9 +18,8 @@ */ import _ from 'lodash'; +import { getServices, getFilterGenerator } from '../../../kibana_services'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import { getFilterGenerator } from 'ui/filter_manager'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, @@ -29,7 +28,7 @@ import { export function QueryParameterActionsProvider(indexPatterns, Private) { - const queryFilter = Private(FilterBarQueryFilterProvider); + const queryFilter = Private(getServices().FilterBarQueryFilterProvider); const filterGen = getFilterGenerator(queryFilter); const setPredecessorCount = (state) => (predecessorCount) => ( diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/constants.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/constants.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/constants.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/constants.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/index.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/index.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/index.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/index.js diff --git a/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/state.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/state.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/query_parameters/state.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/state.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/context/app.html b/src/legacy/core_plugins/kibana/public/discover/angular/context_app.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/context/app.html rename to src/legacy/core_plugins/kibana/public/discover/angular/context_app.html diff --git a/src/legacy/core_plugins/kibana/public/discover/context/app.js b/src/legacy/core_plugins/kibana/public/discover/angular/context_app.js similarity index 92% rename from src/legacy/core_plugins/kibana/public/discover/context/app.js rename to src/legacy/core_plugins/kibana/public/discover/angular/context_app.js index 7754f743632cb..c9856ad794952 100644 --- a/src/legacy/core_plugins/kibana/public/discover/context/app.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context_app.js @@ -18,24 +18,23 @@ */ import _ from 'lodash'; - -import { callAfterBindingsWorkaround } from 'ui/compat'; -import { uiModules } from 'ui/modules'; -import contextAppTemplate from './app.html'; -import './components/action_bar'; -import { getFirstSortableField } from './api/utils/sorting'; +import { getServices, callAfterBindingsWorkaround } from './../kibana_services'; +import contextAppTemplate from './context_app.html'; +import './context/components/action_bar'; +import { getFirstSortableField } from './context/api/utils/sorting'; import { createInitialQueryParametersState, QueryParameterActionsProvider, QUERY_PARAMETER_KEYS, -} from './query_parameters'; +} from './context/query_parameters'; import { createInitialLoadingStatusState, FAILURE_REASONS, LOADING_STATUS, QueryActionsProvider, -} from './query'; -import { timefilter } from 'ui/timefilter'; +} from './context/query'; + +const { uiModules, timefilter } = getServices(); // load directives import '../../../../data/public/legacy'; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/directives/histogram.tsx index 0dca912653f6c..ab336396b5bed 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/directives/histogram.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/angular/directives/histogram.tsx @@ -23,7 +23,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import { npStart } from 'ui/new_platform'; import { AnnotationDomainTypes, @@ -44,12 +43,9 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; - -import chrome from 'ui/chrome'; -// @ts-ignore: path dynamic for kibana -import { timezoneProvider } from 'ui/vis/lib/timezone'; import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes'; import { Subscription } from 'rxjs'; +import { getServices, timezoneProvider } from '../../kibana_services'; export interface DiscoverHistogramProps { chartData: any; @@ -68,12 +64,12 @@ export class DiscoverHistogram extends Component this.setState({ chartsTheme })); } @@ -145,7 +141,7 @@ export class DiscoverHistogram extends Component diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.js b/src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.js index 5f6d32681b50e..b5d3e8a5a01ca 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.js @@ -32,6 +32,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { getServices } from '../../kibana_services'; // eslint-disable-next-line react/prefer-stateless-function export class DiscoverNoResults extends Component { @@ -39,7 +40,6 @@ export class DiscoverNoResults extends Component { shardFailures: PropTypes.array, timeFieldName: PropTypes.string, queryLanguage: PropTypes.string, - getDocLink: PropTypes.func.isRequired, }; render() { @@ -47,7 +47,6 @@ export class DiscoverNoResults extends Component { shardFailures, timeFieldName, queryLanguage, - getDocLink, } = this.props; let shardFailuresMessage; @@ -226,7 +225,7 @@ export class DiscoverNoResults extends Component { queryStringSyntaxLink: ( { + return { + getServices: () => ({ + docLinks: { + links: { + query: { + luceneQuerySyntax: 'documentation-link', + }, + }, + }, + }), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); describe('DiscoverNoResults', () => { describe('props', () => { describe('shardFailures', () => { test('renders failures list when there are failures', () => { - const shardFailures = [{ - index: 'A', - shard: '1', - reason: { reason: 'Awful error' }, - }, { - index: 'B', - shard: '2', - reason: { reason: 'Bad error' }, - }]; + const shardFailures = [ + { + index: 'A', + shard: '1', + reason: { reason: 'Awful error' }, + }, + { + index: 'B', + shard: '2', + reason: { reason: 'Bad error' }, + }, + ]; - const component = renderWithIntl( - ''} - /> - ); + const component = renderWithIntl(); expect(component).toMatchSnapshot(); }); @@ -51,12 +65,7 @@ describe('DiscoverNoResults', () => { test(`doesn't render failures list when there are no failures`, () => { const shardFailures = []; - const component = renderWithIntl( - ''} - /> - ); + const component = renderWithIntl(); expect(component).toMatchSnapshot(); }); @@ -64,12 +73,7 @@ describe('DiscoverNoResults', () => { describe('timeFieldName', () => { test('renders time range feedback', () => { - const component = renderWithIntl( - ''} - /> - ); + const component = renderWithIntl(); expect(component).toMatchSnapshot(); }); @@ -78,10 +82,7 @@ describe('DiscoverNoResults', () => { describe('queryLanguage', () => { test('supports lucene and renders doc link', () => { const component = renderWithIntl( - 'documentation-link'} - /> + 'documentation-link'} /> ); expect(component).toMatchSnapshot(); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/directives/unsupported_index_pattern.js b/src/legacy/core_plugins/kibana/public/discover/angular/directives/unsupported_index_pattern.js index ac26203dafc4a..b1c1c47e39291 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/directives/unsupported_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/directives/unsupported_index_pattern.js @@ -18,7 +18,6 @@ */ import React, { Fragment } from 'react'; - import { EuiCallOut, EuiFlexGroup, diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/angular/discover.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/index.html rename to src/legacy/core_plugins/kibana/public/discover/angular/discover.html diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index 840152fc40ced..ed5049aa912e0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -18,63 +18,65 @@ */ import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; import React from 'react'; -import angular from 'angular'; import { Subscription } from 'rxjs'; import moment from 'moment'; -import chrome from 'ui/chrome'; import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; +import '../saved_searches/saved_searches'; +import '../components/field_chooser/field_chooser'; // doc table -import '../doc_table'; -import { getSort } from '../doc_table/lib/get_sort'; -import { getSortForSearchSource } from '../doc_table/lib/get_sort_for_search_source'; -import * as columnActions from '../doc_table/actions/columns'; -import * as filterActions from '../doc_table/actions/filter'; - -import 'ui/directives/listen'; -import 'ui/visualize'; -import 'ui/fixed_scroll'; -import 'ui/index_patterns'; -import 'ui/state_management/app_state'; -import { timefilter } from 'ui/timefilter'; -import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; -import { toastNotifications } from 'ui/notify'; -import { VisProvider } from 'ui/vis'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; -import { docTitle } from 'ui/doc_title'; -import { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; -import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import indexTemplate from '../index.html'; -import { StateProvider } from 'ui/state_management/state'; -import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; -import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; -import { getFilterGenerator } from 'ui/filter_manager'; - -import { getDocLink } from 'ui/documentation_links'; -import '../components/fetch_error'; -import { getPainlessError } from './get_painless_error'; -import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; -import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; -import { Inspector } from 'ui/inspector'; -import { RequestAdapter } from 'ui/inspector/adapters'; -import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import './doc_table'; +import { getSort } from './doc_table/lib/get_sort'; +import { getSortForSearchSource } from './doc_table/lib/get_sort_for_search_source'; +import * as columnActions from './doc_table/actions/columns'; +import * as filterActions from './doc_table/actions/filter'; + +import indexTemplate from './discover.html'; import { showOpenSearchPanel } from '../top_nav/show_open_search_panel'; -import { tabifyAggResponse } from 'ui/agg_response/tabify'; -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; -import { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; -import 'ui/capabilities/route_setup'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; +import '../components/fetch_error'; +import { getPainlessError } from './get_painless_error'; +import { + angular, + buildVislibDimensions, + getFilterGenerator, + getRequestInspectorStats, + getResponseInspectorStats, + getServices, + getUnhashableStatesProvider, + hasSearchStategyForIndexPattern, + intervalOptions, + isDefaultTypeIndexPattern, + migrateLegacyQuery, + RequestAdapter, + showSaveModal, + showShareContextMenu, + stateMonitorFactory, + subscribeWithScope, + tabifyAggResponse, + vislibSeriesResponseHandlerProvider, + VisProvider, + SavedObjectSaveModal, +} from '../kibana_services'; + +const { + chrome, + docTitle, + FilterBarQueryFilterProvider, + ShareContextMenuExtensionsRegistryProvider, + StateProvider, + timefilter, + toastNotifications, + uiModules, + uiRoutes, +} = getServices(); +import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; import { extractTimeFilter, changeTimeFilter } from '../../../../data/public'; import { start as data } from '../../../../data/public/legacy'; -import { npStart } from 'ui/new_platform'; + const { savedQueryService } = data.search.services; @@ -151,7 +153,7 @@ uiRoutes return savedSearches.get(savedSearchId) .then((savedSearch) => { if (savedSearchId) { - npStart.core.chrome.recentlyAccessed.add( + chrome.recentlyAccessed.add( savedSearch.getFullPath(), savedSearch.title, savedSearchId); @@ -211,8 +213,6 @@ function discoverController( mode: 'absolute', }); }; - - $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; $scope.showInterval = false; $scope.minimumVisibleRows = 50; @@ -354,7 +354,7 @@ function discoverController( }), testId: 'openInspectorButton', run() { - Inspector.open(inspectorAdapters, { + getServices().inspector.open(inspectorAdapters, { title: savedSearch.title }); } @@ -401,12 +401,12 @@ function discoverController( }); if (savedSearch.id && savedSearch.title) { - chrome.breadcrumbs.set([{ + chrome.setBreadcrumbs([{ text: discoverBreadcrumbsTitle, href: '#/discover', }, { text: savedSearch.title }]); } else { - chrome.breadcrumbs.set([{ + chrome.setBreadcrumbs([{ text: discoverBreadcrumbsTitle, }]); } diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/index.html b/src/legacy/core_plugins/kibana/public/discover/angular/doc.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc/index.html rename to src/legacy/core_plugins/kibana/public/discover/angular/doc.html diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/index.ts b/src/legacy/core_plugins/kibana/public/discover/angular/doc.ts similarity index 71% rename from src/legacy/core_plugins/kibana/public/discover/doc/index.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/doc.ts index b969bd6a48126..e6c890c9a66a2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc.ts @@ -16,14 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -import uiRoutes from 'ui/routes'; -import { IndexPatterns } from 'ui/index_patterns'; -import { timefilter } from 'ui/timefilter'; +import { getServices, IndexPatterns } from '../kibana_services'; // @ts-ignore -import { getRootBreadcrumbs } from 'plugins/kibana/discover/breadcrumbs'; -// @ts-ignore -import html from './index.html'; -import './doc_directive'; +import { getRootBreadcrumbs } from '../breadcrumbs'; +import html from './doc.html'; +import { Doc } from '../doc/doc'; +const { uiRoutes, uiModules, wrapInI18nContext, timefilter } = getServices(); +uiModules.get('apps/discover').directive('discoverDoc', function(reactDirective: any) { + return reactDirective( + wrapInI18nContext(Doc), + [ + ['id', { watchDepth: 'value' }], + ['index', { watchDepth: 'value' }], + ['indexPatternId', { watchDepth: 'reference' }], + ['indexPatternService', { watchDepth: 'reference' }], + ['esClient', { watchDepth: 'reference' }], + ], + { restrict: 'E' } + ); +}); uiRoutes // the old, pre 8.0 route, no longer used, keep it to stay compatible diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/actions/filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/actions/filter.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/actions/filter.js rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/actions/filter.js diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/doc_table.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/doc_table.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/doc_table.js rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/doc_table.js diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/get_sort.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/get_sort.js diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/rows_headers.js rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/_doc_table.scss b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/_doc_table.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/_doc_table.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/_doc_table.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/_index.scss b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/_index.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/actions/columns.ts b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/columns.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/actions/columns.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/columns.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/actions/filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/actions/filter.js rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/_index.scss b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/_index.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/_table_header.scss b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/_table_header.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/_table_header.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/_table_header.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/index.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/index.js similarity index 91% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/index.js rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/index.js index e6d638d88bc27..7462de544dbce 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/index.js @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -import { uiModules } from 'ui/modules'; +import { getServices } from '../../../../kibana_services'; import { ToolBarPagerText } from './tool_bar_pager_text'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -import { wrapInI18nContext } from 'ui/i18n'; + +const { wrapInI18nContext, uiModules } = getServices(); const app = uiModules.get('kibana'); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_buttons.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_buttons.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_buttons.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_text.test.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_text.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_text.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_text.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/pager/tool_bar_pager_text.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/pager/tool_bar_pager_text.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.ts b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header.ts similarity index 93% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header.ts index e054120c08474..f447c54507729 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header.ts @@ -17,10 +17,9 @@ * under the License. */ import { wrapInI18nContext } from 'ui/i18n'; -// @ts-ignore -import { uiModules } from 'ui/modules'; +import { getServices } from '../../../kibana_services'; import { TableHeader } from './table_header/table_header'; -const module = uiModules.get('app/discover'); +const module = getServices().uiModules.get('app/discover'); module.directive('kbnTableHeader', function(reactDirective: any, config: any) { return reactDirective( diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/helpers.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/helpers.tsx similarity index 94% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/helpers.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/helpers.tsx index ddf960091d476..80f963c8ccb3e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/helpers.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/helpers.tsx @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - -import { IndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from '../../../../kibana_services'; // @ts-ignore -import { shortenDottedString } from '../../../../../common/utils/shorten_dotted_string'; +import { shortenDottedString } from '../../../../../../common/utils/shorten_dotted_string'; export type SortOrder = [string, 'asc' | 'desc']; export interface ColumnProps { diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/table_header.test.tsx similarity index 98% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/table_header.test.tsx index ea2c65b1b8487..09ba77c7c4999 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/table_header.test.tsx @@ -23,7 +23,7 @@ import { TableHeader } from './table_header'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { SortOrder } from './helpers'; -import { IndexPattern, FieldType } from 'ui/index_patterns'; +import { IndexPattern, FieldType } from '../../../../kibana_services'; function getMockIndexPattern() { return ({ diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/table_header.tsx similarity index 91% rename from src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/table_header.tsx index abc8077afb669..71674710ac855 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_header/table_header.tsx @@ -17,9 +17,8 @@ * under the License. */ import React from 'react'; -import { IndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from '../../../../kibana_services'; // @ts-ignore -import { shortenDottedString } from '../../../../../common/utils/shorten_dotted_string'; import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; @@ -48,7 +47,7 @@ export function TableHeader({ return ( - + {displayedColumns.map(col => { return ( { return { diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_directive.ts b/src/legacy/core_plugins/kibana/public/discover/angular/doc_viewer.ts similarity index 90% rename from src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_directive.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/doc_viewer.ts index fa6145c45f55f..c13c354528413 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_viewer.ts @@ -18,8 +18,10 @@ */ // @ts-ignore -import { uiModules } from 'ui/modules'; -import { DocViewer } from './doc_viewer'; +import { getServices } from '../kibana_services'; +import { DocViewer } from '../doc_viewer/doc_viewer'; + +const { uiModules } = getServices(); uiModules.get('apps/discover').directive('docViewer', (reactDirective: any) => { return reactDirective( diff --git a/src/legacy/ui/public/registry/navbar_extensions.js b/src/legacy/core_plugins/kibana/public/discover/angular/index.ts similarity index 80% rename from src/legacy/ui/public/registry/navbar_extensions.js rename to src/legacy/core_plugins/kibana/public/discover/angular/index.ts index 6c7911c6f0f52..5bae0d9d551e5 100644 --- a/src/legacy/ui/public/registry/navbar_extensions.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/index.ts @@ -16,13 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - -import { uiRegistry } from './_registry'; - -export const NavBarExtensionsRegistryProvider = uiRegistry({ - name: 'navbarExtensions', - index: ['name'], - group: ['appName'], - order: ['order'] -}); - +import './discover'; +import './doc'; +import './context'; +import './doc_viewer'; +import './directives'; diff --git a/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.js b/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts similarity index 88% rename from src/legacy/core_plugins/kibana/public/discover/breadcrumbs.js rename to src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts index 1220c99b5ee56..51e0dcba1cad0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.js +++ b/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts @@ -23,18 +23,18 @@ export function getRootBreadcrumbs() { return [ { text: i18n.translate('kbn.discover.rootBreadcrumb', { - defaultMessage: 'Discover' + defaultMessage: 'Discover', }), - href: '#/discover' - } + href: '#/discover', + }, ]; } -export function getSavedSearchBreadcrumbs($route) { +export function getSavedSearchBreadcrumbs($route: any) { return [ ...getRootBreadcrumbs(), { text: $route.current.locals.savedSearch.id, - } + }, ]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js index 670e9446c6e4f..612ca860f8031 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js @@ -16,21 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -import 'ngreact'; import React, { Fragment } from 'react'; -import { uiModules } from 'ui/modules'; -import { wrapInI18nContext } from 'ui/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { npStart } from 'ui/new_platform'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiCallOut, - EuiCodeBlock, - EuiSpacer, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { getServices } from '../../kibana_services'; +const { uiModules, wrapInI18nContext, chrome } = getServices(); const DiscoverFetchError = ({ fetchError }) => { if (!fetchError) { @@ -40,7 +30,7 @@ const DiscoverFetchError = ({ fetchError }) => { let body; if (fetchError.lang === 'painless') { - const managementUrl = npStart.core.chrome.navLinks.get('kibana:management').url; + const managementUrl = chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; body = ( @@ -51,10 +41,12 @@ const DiscoverFetchError = ({ fetchError }) => { in {managementLink}, under the {scriptedFields} tab." values={{ fetchErrorScript: `'${fetchError.script}'`, - scriptedFields: , + scriptedFields: ( + + ), managementLink: ( { defaultMessage="Management > Index Patterns" /> - ) + ), }} />

@@ -75,16 +67,10 @@ const DiscoverFetchError = ({ fetchError }) => { - + {body} - - {fetchError.error} - + {fetchError.error} @@ -96,4 +82,6 @@ const DiscoverFetchError = ({ fetchError }) => { const app = uiModules.get('apps/discover', ['react']); -app.directive('discoverFetchError', reactDirective => reactDirective(wrapInI18nContext(DiscoverFetchError))); +app.directive('discoverFetchError', reactDirective => + reactDirective(wrapInI18nContext(DiscoverFetchError)) +); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js index f7469d0142d57..cfcb654077152 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js @@ -18,15 +18,15 @@ */ import $ from 'jquery'; +import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { getServices } from '../../kibana_services'; import html from './discover_field.html'; -import _ from 'lodash'; import 'ui/directives/css_truncate'; import 'ui/directives/field_name'; import './string_progress_bar'; import detailsHtml from './lib/detail_views/string.html'; -import { capabilities } from 'ui/capabilities'; -import { uiModules } from 'ui/modules'; +const { uiModules, capabilities } = getServices(); const app = uiModules.get('apps/discover'); app.directive('discoverField', function ($compile) { @@ -78,7 +78,7 @@ app.directive('discoverField', function ($compile) { }; - $scope.canVisualize = capabilities.get().visualize.show; + $scope.canVisualize = capabilities.visualize.show; $scope.toggleDisplay = function (field) { if (field.display) { diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts index 8af23caedd78a..2e7dd3e210ef8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts @@ -17,10 +17,11 @@ * under the License. */ // @ts-ignore -import { uiModules } from 'ui/modules'; -import { wrapInI18nContext } from 'ui/i18n'; +import { getServices } from '../../kibana_services'; import { DiscoverFieldSearch } from './discover_field_search'; +const { wrapInI18nContext, uiModules } = getServices(); + const app = uiModules.get('apps/discover'); app.directive('discoverFieldSearch', function(reactDirective: any) { diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_index_pattern_directive.ts b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_index_pattern_directive.ts index 938d6cc226f2f..5e3f678e388ad 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_index_pattern_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_index_pattern_directive.ts @@ -17,10 +17,11 @@ * under the License. */ // @ts-ignore -import { uiModules } from 'ui/modules'; -import { wrapInI18nContext } from 'ui/i18n'; +import { getServices } from '../../kibana_services'; import { DiscoverIndexPattern } from './discover_index_pattern'; +const { wrapInI18nContext, uiModules } = getServices(); + const app = uiModules.get('apps/discover'); app.directive('discoverIndexPatternSelect', function(reactDirective: any) { diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js index 3e0172dec1ff4..99a63efc0e0fc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js @@ -16,20 +16,22 @@ * specific language governing permissions and limitations * under the License. */ - -import 'ui/directives/css_truncate'; +//field_name directive will be replaced very soon import 'ui/directives/field_name'; import './discover_field'; -import 'ui/angular_ui_select'; import './discover_field_search_directive'; import './discover_index_pattern_directive'; import _ from 'lodash'; import $ from 'jquery'; import rison from 'rison-node'; import { fieldCalculator } from './lib/field_calculator'; -import { FieldList } from 'ui/index_patterns'; -import { uiModules } from 'ui/modules'; +import { + getServices, + FieldList +} from '../../kibana_services'; import fieldChooserTemplate from './field_chooser.html'; + +const { uiModules } = getServices(); const app = uiModules.get('apps/discover'); app.directive('discFieldChooser', function ($location, config, $route) { diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/string_progress_bar.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/string_progress_bar.js index ae00df6dfbbf8..ca3a47cad5075 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/string_progress_bar.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/string_progress_bar.js @@ -16,13 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - - -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; - import React from 'react'; - +import { getServices } from '../../kibana_services'; import { EuiFlexGroup, EuiFlexItem, @@ -31,6 +26,7 @@ import { EuiToolTip, } from '@elastic/eui'; +const { wrapInI18nContext, uiModules } = getServices(); const module = uiModules.get('discover/field_chooser'); function StringFieldProgressBar(props) { diff --git a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js index 95db1b686f7aa..ad68e55e71622 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js @@ -20,7 +20,8 @@ import React, { Fragment, PureComponent } from 'react'; import { EuiButton, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { getServices } from '../../kibana_services'; +const { docLinks } = getServices(); export class HelpMenu extends PureComponent { render() { @@ -31,7 +32,7 @@ export class HelpMenu extends PureComponent { diff --git a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js index aeabff2d97007..58a92193de63e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js @@ -22,7 +22,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { HelpMenu } from './help_menu'; export function addHelpMenuToAppChrome(chrome) { - chrome.helpExtension.set(domElement => { + chrome.setHelpExtension(domElement => { render(, domElement); return () => { unmountComponentAtNode(domElement); diff --git a/src/legacy/core_plugins/kibana/public/discover/context/README.md b/src/legacy/core_plugins/kibana/public/discover/context/README.md new file mode 100644 index 0000000000000..18ba118b4da79 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/context/README.md @@ -0,0 +1,4 @@ +# DISCOVER CONTEXT + +Placeholder for Discover's context functionality, that's currently in [../angular/context](../angular/context). +Once fully de-angularized it should be moved to this location \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx index 6612097620b44..b3efd23ea48d0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx @@ -24,6 +24,27 @@ import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; +jest.mock('../doc_viewer/doc_viewer', () => ({ + DocViewer: 'test', +})); + +jest.mock('../kibana_services', () => { + return { + getServices: () => ({ + metadata: { + branch: 'test', + }, + getDocViewsSorted: () => { + return []; + }, + }), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + // Suppress warnings about "act" until we use React 16.9 /* eslint-disable no-console */ const originalError = console.error; @@ -68,30 +89,30 @@ async function mountDoc(search: () => void, update = false, indexPatternGetter: } describe('Test of of Discover', () => { - it('renders loading msg', async () => { + test('renders loading msg', async () => { const comp = await mountDoc(jest.fn()); expect(findTestSubject(comp, 'doc-msg-loading').length).toBe(1); }); - it('renders IndexPattern notFound msg', async () => { + test('renders IndexPattern notFound msg', async () => { const indexPatternGetter = jest.fn(() => Promise.reject({ savedObjectId: '007' })); const comp = await mountDoc(jest.fn(), true, indexPatternGetter); expect(findTestSubject(comp, 'doc-msg-notFoundIndexPattern').length).toBe(1); }); - it('renders notFound msg', async () => { + test('renders notFound msg', async () => { const search = jest.fn(() => Promise.reject({ status: 404 })); const comp = await mountDoc(search, true); expect(findTestSubject(comp, 'doc-msg-notFound').length).toBe(1); }); - it('renders error msg', async () => { + test('renders error msg', async () => { const search = jest.fn(() => Promise.reject('whatever')); const comp = await mountDoc(search, true); expect(findTestSubject(comp, 'doc-msg-error').length).toBe(1); }); - it('renders elasticsearch hit ', async () => { + test('renders elasticsearch hit ', async () => { const hit = { hits: { total: 1, hits: [{ _id: 1, _source: { test: 1 } }] } }; const search = jest.fn(() => Promise.resolve(hit)); const comp = await mountDoc(search, true); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx index 1f2d2fc532b57..0e0e6ed110ca6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx @@ -19,11 +19,9 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; -import { IndexPatterns } from 'ui/index_patterns'; -import { metadata } from 'ui/metadata'; -import { ElasticSearchHit } from 'ui/registry/doc_views_types'; -import { DocViewer } from '../doc_viewer'; +import { DocViewer } from '../doc_viewer/doc_viewer'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; +import { IndexPatterns, ElasticSearchHit, getServices } from '../kibana_services'; export interface ElasticSearchResult { hits: { @@ -117,7 +115,9 @@ export function Doc(props: DocProps) { values={{ indexName: props.index }} />{' '} { + return { + getServices: () => ({ + docViewsRegistry: { + getDocViewsSorted: (hit: any) => { + return mockGetDocViewsSorted(hit); + }, + }, + }), + }; +}); beforeEach(() => { emptyDocViews(); + jest.clearAllMocks(); }); test('Render with 3 different tabs', () => { diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.tsx index 1c7aba9bcd7b2..aa737ebd8dcf1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { EuiTabbedContent } from '@elastic/eui'; -import { getDocViewsSorted, DocViewRenderProps } from 'ui/registry/doc_views'; +import { getServices, DocViewRenderProps } from '../kibana_services'; import { DocViewerTab } from './doc_viewer_tab'; /** @@ -28,21 +28,24 @@ import { DocViewerTab } from './doc_viewer_tab'; * a `render` function. */ export function DocViewer(renderProps: DocViewRenderProps) { - const tabs = getDocViewsSorted(renderProps.hit).map(({ title, render, component }, idx) => { - return { - id: title, - name: title, - content: ( - - ), - }; - }); + const { docViewsRegistry } = getServices(); + const tabs = docViewsRegistry + .getDocViewsSorted(renderProps.hit) + .map(({ title, render, component }, idx) => { + return { + id: title, + name: title, + content: ( + + ), + }; + }); if (!tabs.length) { // There there's a minimum of 2 tabs active in Discover. diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.test.tsx index 3bb59a8dc958c..5fa2d24dfa04c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { DocViewRenderTab } from './doc_viewer_render_tab'; -import { DocViewRenderProps } from 'ui/registry/doc_views'; +import { DocViewRenderProps } from '../kibana_services'; test('Mounting and unmounting DocViewerRenderTab', () => { const unmountFn = jest.fn(); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.tsx index 185ff163dad2a..750ef6b6061e1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.tsx @@ -17,7 +17,7 @@ * under the License. */ import React, { useRef, useEffect } from 'react'; -import { DocViewRenderFn, DocViewRenderProps } from 'ui/registry/doc_views'; +import { DocViewRenderFn, DocViewRenderProps } from '../kibana_services'; interface Props { render: DocViewRenderFn; diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_tab.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_tab.tsx index 0b25421d8aff3..3721ba5818d41 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_tab.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_tab.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; -import { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; +import { DocViewRenderProps, DocViewRenderFn } from '../kibana_services'; import { DocViewRenderTab } from './doc_viewer_render_tab'; import { DocViewerError } from './doc_viewer_render_error'; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/constants.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/constants.ts new file mode 100644 index 0000000000000..cdb8a6ad3ad46 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/constants.ts @@ -0,0 +1,19 @@ +/* + * 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 SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts index 3138008f3e3a0..beeb6a7338f9d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts @@ -20,3 +20,4 @@ export * from './types'; export * from './search_embeddable_factory'; export * from './search_embeddable'; +export { SEARCH_EMBEDDABLE_TYPE } from './constants'; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index eaec11ff893ed..5f3ebd6d22e24 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -16,42 +16,38 @@ * specific language governing permissions and limitations * under the License. */ - -// @ts-ignore -import { getFilterGenerator } from 'ui/filter_manager'; -import angular from 'angular'; import _ from 'lodash'; -import { SearchSource } from 'ui/courier'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from 'ui/courier/utils/courier_inspector_utils'; -import { IndexPattern } from 'ui/index_patterns'; -import { RequestAdapter } from 'ui/inspector/adapters'; -import { Adapters } from 'ui/inspector/types'; -import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; +import { Subscription } from 'rxjs'; import { Filter, FilterStateStore } from '@kbn/es-query'; -import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; -import { TimeRange } from 'src/plugins/data/public'; import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { setup as data } from '../../../../data/public/legacy'; -import { Query, onlyDisabledFiltersChanged, getTime } from '../../../../data/public'; +import { getTime, onlyDisabledFiltersChanged, Query } from '../../../../data/public'; import { APPLY_FILTER_TRIGGER, - Embeddable, Container, + Embeddable, } from '../../../../embeddable_api/public/np_ready/public'; -import * as columnActions from '../doc_table/actions/columns'; +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 '../doc_table/components/table_header/helpers'; -import { getSortForSearchSource } from '../doc_table/lib/get_sort_for_search_source'; - -const config = chrome.getUiSettingsClient(); +import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; +import { getSortForSearchSource } from '../angular/doc_table/lib/get_sort_for_search_source'; +import { + Adapters, + angular, + getFilterGenerator, + getRequestInspectorStats, + getResponseInspectorStats, + getServices, + IndexPattern, + RequestAdapter, + SearchSource, +} from '../kibana_services'; +import { TimeRange } from '../../../../../../plugins/data/public'; +import { SEARCH_EMBEDDABLE_TYPE } from './constants'; interface SearchScope extends ng.IScope { columns?: string[]; @@ -92,8 +88,6 @@ interface SearchEmbeddableConfig { queryFilter: unknown; } -export const SEARCH_EMBEDDABLE_TYPE = 'search'; - export class SearchEmbeddable extends Embeddable implements ISearchEmbeddable { private readonly savedSearch: SavedSearch; @@ -277,7 +271,7 @@ export class SearchEmbeddable extends Embeddable if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - searchSource.setField('size', config.get('discover:sampleSize')); + searchSource.setField('size', getServices().uiSettings.get('discover:sampleSize')); searchSource.setField( 'sort', getSortForSearchSource(this.searchScope.sort, this.searchScope.indexPattern) @@ -319,7 +313,7 @@ export class SearchEmbeddable extends Embeddable // If the fetch was aborted, no need to surface this in the UI if (error.name === 'AbortError') return; - toastNotifications.addError(error, { + getServices().toastNotifications.addError(error, { title: i18n.translate('kbn.embeddable.errorTitle', { defaultMessage: 'Error fetching data', }), diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index d7b51b39e2a16..1939cc7060621 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -16,24 +16,21 @@ * specific language governing permissions and limitations * under the License. */ - -import '../doc_table'; -import { capabilities } from 'ui/capabilities'; -import { npStart, npSetup } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; -import { TimeRange } from 'src/plugins/data/public'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; +import { i18n } from '@kbn/i18n'; import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import '../angular/doc_table'; +import { getServices } from '../kibana_services'; import { EmbeddableFactory, ErrorEmbeddable, Container, } from '../../../../../../plugins/embeddable/public'; +import { TimeRange } from '../../../../../../plugins/data/public'; import { SavedSearchLoader } from '../types'; -import { SearchEmbeddable, SEARCH_EMBEDDABLE_TYPE } from './search_embeddable'; +import { SearchEmbeddable } from './search_embeddable'; import { SearchInput, SearchOutput } from './types'; +import { SEARCH_EMBEDDABLE_TYPE } from './constants'; export class SearchEmbeddableFactory extends EmbeddableFactory< SearchInput, @@ -55,7 +52,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< } public isEditable() { - return capabilities.get().discover.save as boolean; + return getServices().capabilities.discover.save as boolean; } public canCreateNew() { @@ -73,16 +70,18 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< input: Partial & { id: string; timeRange: TimeRange }, parent?: Container ): Promise { - const $injector = await chrome.dangerouslyGetActiveInjector(); + const $injector = await getServices().getInjector(); const $compile = $injector.get('$compile'); const $rootScope = $injector.get('$rootScope'); const searchLoader = $injector.get('savedSearches'); - const editUrl = chrome.addBasePath(`/app/kibana${searchLoader.urlFor(savedObjectId)}`); + const editUrl = await getServices().addBasePath( + `/app/kibana${searchLoader.urlFor(savedObjectId)}` + ); const Private = $injector.get('Private'); - const queryFilter = Private(FilterBarQueryFilterProvider); + const queryFilter = Private(getServices().FilterBarQueryFilterProvider); try { const savedObject = await searchLoader.get(savedObjectId); return new SearchEmbeddable( @@ -92,7 +91,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< $compile, editUrl, queryFilter, - editable: capabilities.get().discover.save as boolean, + editable: getServices().capabilities.discover.save as boolean, indexPatterns: _.compact([savedObject.searchSource.getField('index')]), }, input, @@ -110,5 +109,5 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< } } -const factory = new SearchEmbeddableFactory(npStart.plugins.uiActions.executeTriggerActions); -npSetup.plugins.embeddable.registerEmbeddableFactory(factory.type, factory); +const factory = new SearchEmbeddableFactory(getServices().uiActions.executeTriggerActions); +getServices().embeddable.registerEmbeddableFactory(factory.type, factory); diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts index bc46cdbe82981..db8d2afc7aff3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts @@ -17,13 +17,13 @@ * under the License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; import { TimeRange } from 'src/plugins/data/public'; import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from 'src/plugins/embeddable/public'; +import { StaticIndexPattern } from '../kibana_services'; import { SavedSearch } from '../types'; -import { SortOrder } from '../doc_table/components/table_header/helpers'; +import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/doc_directive.ts b/src/legacy/core_plugins/kibana/public/discover/helpers/register_feature.ts similarity index 52% rename from src/legacy/core_plugins/kibana/public/discover/doc/doc_directive.ts rename to src/legacy/core_plugins/kibana/public/discover/helpers/register_feature.ts index 3ee510f47ce5b..eb8c2aec91558 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc/doc_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/helpers/register_feature.ts @@ -16,21 +16,26 @@ * specific language governing permissions and limitations * under the License. */ -import { wrapInI18nContext } from 'ui/i18n'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { Doc } from './doc'; +import { i18n } from '@kbn/i18n'; +import { + FeatureCatalogueRegistryProvider, + FeatureCatalogueCategory, +} from 'ui/registry/feature_catalogue'; -uiModules.get('apps/discover').directive('discoverDoc', function(reactDirective: any) { - return reactDirective( - wrapInI18nContext(Doc), - [ - ['id', { watchDepth: 'value' }], - ['index', { watchDepth: 'value' }], - ['indexPatternId', { watchDepth: 'reference' }], - ['indexPatternService', { watchDepth: 'reference' }], - ['esClient', { watchDepth: 'reference' }], - ], - { restrict: 'E' } - ); -}); +export function registerFeature() { + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'discover', + title: i18n.translate('kbn.discover.discoverTitle', { + defaultMessage: 'Discover', + }), + description: i18n.translate('kbn.discover.discoverDescription', { + defaultMessage: 'Interactively explore your data by querying and filtering raw documents.', + }), + icon: 'discoverApp', + path: '/app/kibana#/discover', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }; + }); +} diff --git a/src/legacy/core_plugins/kibana/public/discover/index.js b/src/legacy/core_plugins/kibana/public/discover/index.js deleted file mode 100644 index e509936306275..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/index.js +++ /dev/null @@ -1,46 +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 './saved_searches/saved_searches'; -import { i18n } from '@kbn/i18n'; - -import './angular/directives'; -import 'ui/collapsible_sidebar'; -import './components/field_chooser/field_chooser'; -import './angular/discover'; -import './doc_table/components/table_row'; -import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -import './doc'; -import './context'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'discover', - title: i18n.translate('kbn.discover.discoverTitle', { - defaultMessage: 'Discover', - }), - description: i18n.translate('kbn.discover.discoverDescription', { - defaultMessage: 'Interactively explore your data by querying and filtering raw documents.', - }), - icon: 'discoverApp', - path: '/app/kibana#/discover', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/index.ts b/src/legacy/core_plugins/kibana/public/discover/index.ts new file mode 100644 index 0000000000000..35e48598f07a8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/index.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { DiscoverPlugin, DiscoverSetup, DiscoverStart } from './plugin'; + +// Core will be looking for this when loading our plugin in the new platform +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new DiscoverPlugin(initializerContext); +}; + +const pluginInstance = plugin({} as PluginInitializerContext); +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts new file mode 100644 index 0000000000000..dd0674073f442 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -0,0 +1,121 @@ +/* + * 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 'ui/collapsible_sidebar'; +import 'ui/directives/listen'; +import 'ui/fixed_scroll'; +import 'ui/directives/css_truncate'; + +import { npStart } from 'ui/new_platform'; +import chromeLegacy from 'ui/chrome'; +import angular from 'angular'; // just used in embeddables and discover controller +import uiRoutes from 'ui/routes'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { SearchSource } from 'ui/courier'; +// @ts-ignore +import { StateProvider } from 'ui/state_management/state'; +// @ts-ignore +import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; +import { timefilter } from 'ui/timefilter'; +import { ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; +// @ts-ignore +import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; +import { wrapInI18nContext } from 'ui/i18n'; +// @ts-ignore +import { docTitle } from 'ui/doc_title'; +// @ts-ignore +import * as docViewsRegistry from 'ui/registry/doc_views'; + +const services = { + // new plattform + addBasePath: npStart.core.http.basePath.prepend, + capabilities: npStart.core.application.capabilities, + chrome: npStart.core.chrome, + docLinks: npStart.core.docLinks, + eui_utils: npStart.plugins.eui_utils, + inspector: npStart.plugins.inspector, + metadata: npStart.core.injectedMetadata.getLegacyMetadata(), + toastNotifications: npStart.core.notifications.toasts, + uiSettings: npStart.core.uiSettings, + uiActions: npStart.plugins.uiActions, + embeddable: npStart.plugins.embeddable, + // legacy + docTitle, + docViewsRegistry, + FilterBarQueryFilterProvider, + getInjector: () => { + return chromeLegacy.dangerouslyGetActiveInjector(); + }, + SavedObjectRegistryProvider, + SavedObjectProvider, + SearchSource, + ShareContextMenuExtensionsRegistryProvider, + StateProvider, + timefilter, + uiModules, + uiRoutes, + wrapInI18nContext, +}; +export function getServices() { + return services; +} + +// EXPORT legacy static dependencies +export { angular }; +export { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; +// @ts-ignore +export { callAfterBindingsWorkaround } from 'ui/compat'; +// @ts-ignore +export { getFilterGenerator } from 'ui/filter_manager'; +export { + getRequestInspectorStats, + getResponseInspectorStats, +} from 'ui/courier/utils/courier_inspector_utils'; +// @ts-ignore +export { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; +// @ts-ignore +export { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; +// @ts-ignore +export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; +// @ts-ignore +export { RequestAdapter } from 'ui/inspector/adapters'; +export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; +export { FieldList } from 'ui/index_patterns'; +export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +export { showShareContextMenu } from 'ui/share'; +export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; +export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; +// @ts-ignore +export { timezoneProvider } from 'ui/vis/lib/timezone'; +// @ts-ignore +export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; +// @ts-ignore +export { tabifyAggResponse } from 'ui/agg_response/tabify'; +// @ts-ignore +export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; + +// EXPORT types +export { VisProvider } from 'ui/vis'; +export { StaticIndexPattern, IndexPatterns, IndexPattern, FieldType } from 'ui/index_patterns'; +export { SearchSource } from 'ui/courier'; +export { ElasticSearchHit } from 'ui/registry/doc_views_types'; +export { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; +export { Adapters } from 'ui/inspector/types'; diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts new file mode 100644 index 0000000000000..873c429bf705d --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; +import { IUiActionsStart } from 'src/plugins/ui_actions/public'; +import { registerFeature } from './helpers/register_feature'; +import './kibana_services'; +import { + Start as EmbeddableStart, + Setup as EmbeddableSetup, +} from '../../../../../plugins/embeddable/public'; + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type DiscoverSetup = void; +export type DiscoverStart = void; +interface DiscoverSetupPlugins { + uiActions: IUiActionsStart; + embeddable: EmbeddableSetup; +} +interface DiscoverStartPlugins { + uiActions: IUiActionsStart; + embeddable: EmbeddableStart; +} + +export class DiscoverPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { + registerFeature(); + require('./angular'); + } + + start(core: CoreStart, plugins: DiscoverStartPlugins): DiscoverStart { + // TODO enable this when possible, seems it broke a functional test: + // dashboard mode Dashboard View Mode Dashboard viewer can paginate on a saved search + // const factory = new SearchEmbeddableFactory(plugins.uiActions.executeTriggerActions); + // plugins.embeddable.registerEmbeddableFactory(factory.type, factory); + } + + stop() {} +} diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js index 3903dc0845450..9bbc5baf4fc22 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js @@ -17,10 +17,10 @@ * under the License. */ -import 'ui/notify'; -import { uiModules } from 'ui/modules'; import { createLegacyClass } from 'ui/utils/legacy_class'; -import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; +import { getServices } from '../kibana_services'; + +const { uiModules, SavedObjectProvider } = getServices(); const module = uiModules.get('discover/saved_searches', []); @@ -40,7 +40,7 @@ module.factory('SavedSearch', function (Private) { columns: [], hits: 0, sort: [], - version: 1 + version: 1, }, }); @@ -55,7 +55,7 @@ module.factory('SavedSearch', function (Private) { hits: 'integer', columns: 'keyword', sort: 'keyword', - version: 'integer' + version: 'integer', }; // Order these fields to the top, the rest are alphabetical diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_search_register.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_search_register.js index 8460ccf923cf3..9554642c225fd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_search_register.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_search_register.js @@ -17,10 +17,10 @@ * under the License. */ -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +import { getServices } from '../kibana_services'; import './saved_searches'; -SavedObjectRegistryProvider.register((savedSearches) => { +getServices().SavedObjectRegistryProvider.register((savedSearches) => { return savedSearches; }); diff --git a/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js b/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js index f29ebe6a47141..0c3b52fbf0640 100644 --- a/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js +++ b/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js @@ -19,11 +19,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; import rison from 'rison-node'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiFlexGroup, @@ -34,6 +32,7 @@ import { EuiFlyoutBody, EuiTitle, } from '@elastic/eui'; +import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; const SEARCH_OBJECT_TYPE = 'search'; diff --git a/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.test.js b/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.test.js index 2bec532110379..3531088e3847c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.test.js @@ -20,6 +20,14 @@ import React from 'react'; import { shallow } from 'enzyme'; +jest.mock('../kibana_services', () => { + return { + getServices: () => ({ + SavedObjectFinder: jest.fn() + }), + }; +}); + import { OpenSearchPanel, } from './open_search_panel'; diff --git a/src/legacy/core_plugins/kibana/public/discover/types.d.ts b/src/legacy/core_plugins/kibana/public/discover/types.d.ts index f285e94369893..7d8740243ec02 100644 --- a/src/legacy/core_plugins/kibana/public/discover/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/types.d.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SearchSource } from 'ui/courier'; -import { SortOrder } from './doc_table/components/table_header/helpers'; +import { SearchSource } from './kibana_services'; +import { SortOrder } from './angular/doc_table/components/table_header/helpers'; export interface SavedSearch { readonly id: string; diff --git a/src/legacy/core_plugins/kibana/public/home/index.js b/src/legacy/core_plugins/kibana/public/home/index.js index 829d1ef8f0ba4..8a8680eeba47c 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.js +++ b/src/legacy/core_plugins/kibana/public/home/index.js @@ -36,12 +36,12 @@ const homeTitle = i18n.translate('kbn.home.breadcrumbs.homeTitle', { defaultMess function getRoute() { return { template, - controller($scope) { - const { chrome, addBasePath, getFeatureCatalogueRegistryProvider } = getServices(); - getFeatureCatalogueRegistryProvider().then(catalogue => { - $scope.directories = catalogue.inTitleOrder; - $scope.$digest(); - }); + resolve: { + directories: () => getServices().getFeatureCatalogueRegistryProvider().then(catalogue => catalogue.inTitleOrder) + }, + controller($scope, $route) { + const { chrome, addBasePath } = getServices(); + $scope.directories = $route.current.locals.directories; $scope.recentlyAccessed = chrome.recentlyAccessed.get().map(item => { item.link = addBasePath(item.link); return item; diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/consul_metrics/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/consul_metrics/screenshot.png new file mode 100644 index 0000000000000..90aaa7477e8eb Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/consul_metrics/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/consul.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/consul.svg new file mode 100644 index 0000000000000..28bbadd24c8a6 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/consul.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts index b5e112a489ce4..8aedc6f7332dc 100644 --- a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts +++ b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts @@ -19,7 +19,7 @@ import { SavedObject, SavedObjectAttributes } from 'src/core/server'; import { collectReferencesDeep } from './collect_references_deep'; -import { SavedObjectsClientMock } from '../../../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../core/server/mocks'; const data: Array> = [ { @@ -102,7 +102,7 @@ const data: Array> = [ ]; test('collects dashboard and all dependencies', async () => { - const savedObjectClient = SavedObjectsClientMock.create(); + const savedObjectClient = savedObjectsClientMock.create(); savedObjectClient.bulkGet.mockImplementation(objects => { if (!objects) { throw new Error('Invalid test data'); diff --git a/src/legacy/core_plugins/kibana/server/tutorials/consul_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/consul_metrics/index.js new file mode 100644 index 0000000000000..8823fe5ee5067 --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/consul_metrics/index.js @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; + +export function consulMetricsSpecProvider(server, context) { + const moduleName = 'consul'; + return { + id: 'consulMetrics', + name: i18n.translate('kbn.server.tutorials.consulMetrics.nameTitle', { + defaultMessage: 'Consul metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.consulMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from the Consul server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.consulMetrics.longDescription', { + defaultMessage: 'The `consul` Metricbeat module fetches monitoring metrics from Consul. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-consul.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/consul.svg', + artifacts: { + dashboards: [ + { + id: '496910f0-b952-11e9-a579-f5c0a5d81340', + linkLabel: i18n.translate('kbn.server.tutorials.consulMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'Consul metrics dashboard', + }), + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-consul.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/consul_metrics/screenshot.png', + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 0299d00856c64..70bc0f4d3c9eb 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -77,6 +77,7 @@ import { ciscoLogsSpecProvider } from './cisco_logs'; import { envoyproxyLogsSpecProvider } from './envoyproxy_logs'; import { couchdbMetricsSpecProvider } from './couchdb_metrics'; import { emsBoundariesSpecProvider } from './ems'; +import { consulMetricsSpecProvider } from './consul_metrics'; export function registerTutorials(server) { server.registerTutorial(systemLogsSpecProvider); @@ -139,4 +140,5 @@ export function registerTutorials(server) { server.registerTutorial(envoyproxyLogsSpecProvider); server.registerTutorial(couchdbMetricsSpecProvider); server.registerTutorial(emsBoundariesSpecProvider); + server.registerTutorial(consulMetricsSpecProvider); } diff --git a/src/legacy/core_plugins/kibana_react/public/index.scss b/src/legacy/core_plugins/kibana_react/public/index.scss index 14922ca8ee8eb..14b4687c459e1 100644 --- a/src/legacy/core_plugins/kibana_react/public/index.scss +++ b/src/legacy/core_plugins/kibana_react/public/index.scss @@ -1,5 +1,3 @@ @import 'src/legacy/ui/public/styles/styling_constants'; -@import './top_nav_menu/index'; - @import './markdown/index'; diff --git a/src/legacy/core_plugins/kibana_react/public/index.ts b/src/legacy/core_plugins/kibana_react/public/index.ts index 2497671ca98d2..7e68b6c3886ff 100644 --- a/src/legacy/core_plugins/kibana_react/public/index.ts +++ b/src/legacy/core_plugins/kibana_react/public/index.ts @@ -23,6 +23,4 @@ // of the ExpressionExectorService /** @public types */ -export { TopNavMenu, TopNavMenuData } from './top_nav_menu'; - export { Markdown, MarkdownSimple } from './markdown'; diff --git a/src/legacy/core_plugins/navigation/index.ts b/src/legacy/core_plugins/navigation/index.ts new file mode 100644 index 0000000000000..32d5f040760c6 --- /dev/null +++ b/src/legacy/core_plugins/navigation/index.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { Legacy } from '../../../../kibana'; + +// eslint-disable-next-line import/no-default-export +export default function NavigationPlugin(kibana: any) { + const config: Legacy.PluginSpecOptions = { + id: 'navigation', + require: [], + publicDir: resolve(__dirname, 'public'), + config: (Joi: any) => { + return Joi.object({ + enabled: Joi.boolean().default(true), + }).default(); + }, + init: (server: Legacy.Server) => ({}), + uiExports: { + injectDefaultVars: () => ({}), + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }, + }; + + return new kibana.Plugin(config); +} diff --git a/src/legacy/core_plugins/navigation/package.json b/src/legacy/core_plugins/navigation/package.json new file mode 100644 index 0000000000000..8fddb8e6aeced --- /dev/null +++ b/src/legacy/core_plugins/navigation/package.json @@ -0,0 +1,4 @@ +{ + "name": "navigation", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/navigation/public/index.scss b/src/legacy/core_plugins/navigation/public/index.scss new file mode 100644 index 0000000000000..4c969b1a37e8c --- /dev/null +++ b/src/legacy/core_plugins/navigation/public/index.scss @@ -0,0 +1,3 @@ +@import 'src/legacy/ui/public/styles/styling_constants'; + +@import './top_nav_menu/index'; diff --git a/src/legacy/core_plugins/navigation/public/index.ts b/src/legacy/core_plugins/navigation/public/index.ts new file mode 100644 index 0000000000000..439405cc90b57 --- /dev/null +++ b/src/legacy/core_plugins/navigation/public/index.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// TODO these are imports from the old plugin world. +// Once the new platform is ready, they can get removed +// and handled by the platform itself in the setup method +// of the ExpressionExectorService + +/** @public types */ +export { TopNavMenu, TopNavMenuData } from './top_nav_menu'; +export { NavigationSetup, NavigationStart } from './plugin'; + +import { NavigationPlugin as Plugin } from './plugin'; +export function plugin() { + return new Plugin(); +} diff --git a/src/legacy/core_plugins/navigation/public/legacy.ts b/src/legacy/core_plugins/navigation/public/legacy.ts new file mode 100644 index 0000000000000..1783514e6fbdc --- /dev/null +++ b/src/legacy/core_plugins/navigation/public/legacy.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { npSetup, npStart } from 'ui/new_platform'; +import { start as dataShim } from '../../data/public/legacy'; +import { plugin } from '.'; + +const navPlugin = plugin(); + +export const setup = navPlugin.setup(npSetup.core); + +export const start = navPlugin.start(npStart.core, { + data: dataShim, +}); diff --git a/src/legacy/core_plugins/navigation/public/plugin.ts b/src/legacy/core_plugins/navigation/public/plugin.ts new file mode 100644 index 0000000000000..65a0902dec986 --- /dev/null +++ b/src/legacy/core_plugins/navigation/public/plugin.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { TopNavMenuExtensionsRegistry, TopNavMenuExtensionsRegistrySetup } from './top_nav_menu'; +import { createTopNav } from './top_nav_menu/create_top_nav_menu'; +import { TopNavMenuProps } from './top_nav_menu/top_nav_menu'; +import { DataStart } from '../../data/public'; + +/** + * Interface for this plugin's returned `setup` contract. + * + * @public + */ +export interface NavigationSetup { + registerMenuItem: TopNavMenuExtensionsRegistrySetup['register']; +} + +/** + * Interface for this plugin's returned `start` contract. + * + * @public + */ +export interface NavigationStart { + ui: { + TopNavMenu: React.ComponentType; + }; +} + +export interface NavigationPluginStartDependencies { + data: DataStart; +} + +export class NavigationPlugin implements Plugin { + private readonly topNavMenuExtensionsRegistry: TopNavMenuExtensionsRegistry = new TopNavMenuExtensionsRegistry(); + + public setup(core: CoreSetup): NavigationSetup { + return { + registerMenuItem: this.topNavMenuExtensionsRegistry.register.bind( + this.topNavMenuExtensionsRegistry + ), + }; + } + + public start(core: CoreStart, { data }: NavigationPluginStartDependencies): NavigationStart { + const extensions = this.topNavMenuExtensionsRegistry.getAll(); + + return { + ui: { + TopNavMenu: createTopNav(data, extensions), + }, + }; + } + + public stop() { + this.topNavMenuExtensionsRegistry.clear(); + } +} diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/_index.scss b/src/legacy/core_plugins/navigation/public/top_nav_menu/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/_index.scss rename to src/legacy/core_plugins/navigation/public/top_nav_menu/_index.scss diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx new file mode 100644 index 0000000000000..fa0e5fcac7407 --- /dev/null +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx @@ -0,0 +1,31 @@ +/* + * 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 { TopNavMenuProps, TopNavMenu } from './top_nav_menu'; +import { TopNavMenuData } from './top_nav_menu_data'; +import { DataStart } from '../../../../core_plugins/data/public'; + +export function createTopNav(data: DataStart, extraConfig: TopNavMenuData[]) { + return (props: TopNavMenuProps) => { + const config = (props.config || []).concat(extraConfig); + + return ; + }; +} diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/index.ts b/src/legacy/core_plugins/navigation/public/top_nav_menu/index.ts similarity index 94% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/index.ts rename to src/legacy/core_plugins/navigation/public/top_nav_menu/index.ts index b45baaf0e4711..dd139bbd6410f 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/index.ts +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/index.ts @@ -19,3 +19,4 @@ export { TopNavMenu } from './top_nav_menu'; export { TopNavMenuData } from './top_nav_menu_data'; +export * from './top_nav_menu_extensions_registry'; diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx similarity index 88% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx rename to src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 21c5cef4ae925..803119bdac119 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -27,21 +27,11 @@ const timefilterSetupMock = timefilterServiceMock.createSetupContract(); jest.mock('ui/new_platform'); -jest.mock('../../../../../../src/legacy/core_plugins/data/public/legacy', () => ({ - start: { - ui: { - SearchBar: () => {}, - }, - }, - setup: {}, -})); - -jest.mock('../../../../core_plugins/data/public', () => { - return { +const dataShim = { + ui: { SearchBar: () =>
, - SearchBarProps: {}, - }; -}); + }, +}; describe('TopNavMenu', () => { const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem'; @@ -84,7 +74,12 @@ describe('TopNavMenu', () => { it('Should render search bar', () => { const component = shallowWithIntl( - + ); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx similarity index 89% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx rename to src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index aec91c2aa6bc6..14599e76470c0 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,13 +24,13 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBarProps } from '../../../../core_plugins/data/public'; -import { start as data } from '../../../data/public/legacy'; +import { SearchBarProps, DataStart } from '../../../../core_plugins/data/public'; -type Props = Partial & { +export type TopNavMenuProps = Partial & { appName: string; config?: TopNavMenuData[]; showSearchBar?: boolean; + data?: DataStart; }; /* @@ -42,8 +42,7 @@ type Props = Partial & { * **/ -export function TopNavMenu(props: Props) { - const { SearchBar } = data.ui; +export function TopNavMenu(props: TopNavMenuProps) { const { config, showSearchBar, ...searchBarProps } = props; function renderItems() { if (!config) return; @@ -58,7 +57,8 @@ export function TopNavMenu(props: Props) { function renderSearchBar() { // Validate presense of all required fields - if (!showSearchBar) return; + if (!showSearchBar || !props.data) return; + const { SearchBar } = props.data.ui; return ; } diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu_data.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx similarity index 100% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu_data.tsx rename to src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts new file mode 100644 index 0000000000000..c3eab3cce18e6 --- /dev/null +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TopNavMenuData } from './top_nav_menu_data'; + +export class TopNavMenuExtensionsRegistry { + private menuItems: TopNavMenuData[]; + + constructor() { + this.menuItems = []; + } + + /** @public **/ + // Items registered into this registry will be appended to any TopNavMenu rendered in any application. + public register(menuItem: TopNavMenuData) { + this.menuItems.push(menuItem); + } + + /** @internal **/ + public getAll() { + return this.menuItems; + } + + /** @internal **/ + public clear() { + this.menuItems.length = 0; + } +} + +export type TopNavMenuExtensionsRegistrySetup = Pick; diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu_item.test.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu_item.test.tsx rename to src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.test.tsx diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu_item.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx similarity index 100% rename from src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu_item.tsx rename to src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx diff --git a/src/legacy/server/saved_objects/routes/bulk_create.test.ts b/src/legacy/server/saved_objects/routes/bulk_create.test.ts index 1e041bb28f75f..b49554995aab6 100644 --- a/src/legacy/server/saved_objects/routes/bulk_create.test.ts +++ b/src/legacy/server/saved_objects/routes/bulk_create.test.ts @@ -22,11 +22,11 @@ import { createMockServer } from './_mock_server'; import { createBulkCreateRoute } from './bulk_create'; // Disable lint errors for imports from src/core/* until SavedObjects migration is complete // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsClientMock } from '../../../../core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from '../../../../core/server/saved_objects/service/saved_objects_client.mock'; describe('POST /api/saved_objects/_bulk_create', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { savedObjectsClient.bulkCreate.mockImplementation(() => Promise.resolve('' as any)); diff --git a/src/legacy/server/saved_objects/routes/bulk_get.test.ts b/src/legacy/server/saved_objects/routes/bulk_get.test.ts index 546164be65c9f..e154649e2cf04 100644 --- a/src/legacy/server/saved_objects/routes/bulk_get.test.ts +++ b/src/legacy/server/saved_objects/routes/bulk_get.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createBulkGetRoute } from './bulk_get'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('POST /api/saved_objects/_bulk_get', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { savedObjectsClient.bulkGet.mockImplementation(() => diff --git a/src/legacy/server/saved_objects/routes/bulk_update.test.ts b/src/legacy/server/saved_objects/routes/bulk_update.test.ts index ee74ddfc535d2..dc21ab08035ce 100644 --- a/src/legacy/server/saved_objects/routes/bulk_update.test.ts +++ b/src/legacy/server/saved_objects/routes/bulk_update.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createBulkUpdateRoute } from './bulk_update'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('PUT /api/saved_objects/_bulk_update', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { server = createMockServer(); diff --git a/src/legacy/server/saved_objects/routes/create.test.ts b/src/legacy/server/saved_objects/routes/create.test.ts index 85096228c3175..4f096a9ee5c93 100644 --- a/src/legacy/server/saved_objects/routes/create.test.ts +++ b/src/legacy/server/saved_objects/routes/create.test.ts @@ -20,7 +20,7 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createCreateRoute } from './create'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('POST /api/saved_objects/{type}', () => { let server: Hapi.Server; @@ -32,7 +32,7 @@ describe('POST /api/saved_objects/{type}', () => { references: [], attributes: {}, }; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { savedObjectsClient.create.mockImplementation(() => Promise.resolve(clientResponse)); diff --git a/src/legacy/server/saved_objects/routes/delete.test.ts b/src/legacy/server/saved_objects/routes/delete.test.ts index 9e2adcf9d3b91..f3e5e83771471 100644 --- a/src/legacy/server/saved_objects/routes/delete.test.ts +++ b/src/legacy/server/saved_objects/routes/delete.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createDeleteRoute } from './delete'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('DELETE /api/saved_objects/{type}/{id}', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { savedObjectsClient.delete.mockImplementation(() => Promise.resolve('{}')); diff --git a/src/legacy/server/saved_objects/routes/export.test.ts b/src/legacy/server/saved_objects/routes/export.test.ts index 2670535ab995e..93ca3a419e6df 100644 --- a/src/legacy/server/saved_objects/routes/export.test.ts +++ b/src/legacy/server/saved_objects/routes/export.test.ts @@ -28,14 +28,14 @@ import * as exportMock from '../../../../core/server/saved_objects/export'; import { createMockServer } from './_mock_server'; import { createExportRoute } from './export'; import { createListStream } from '../../../utils/streams'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; const getSortedObjectsForExport = exportMock.getSortedObjectsForExport as jest.Mock; describe('POST /api/saved_objects/_export', () => { let server: Hapi.Server; const savedObjectsClient = { - ...SavedObjectsClientMock.create(), + ...savedObjectsClientMock.create(), errors: {} as any, }; diff --git a/src/legacy/server/saved_objects/routes/find.test.ts b/src/legacy/server/saved_objects/routes/find.test.ts index 89cd0dd28d035..4bf5f57fec199 100644 --- a/src/legacy/server/saved_objects/routes/find.test.ts +++ b/src/legacy/server/saved_objects/routes/find.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createFindRoute } from './find'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('GET /api/saved_objects/_find', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); const clientResponse = { total: 0, diff --git a/src/legacy/server/saved_objects/routes/get.test.ts b/src/legacy/server/saved_objects/routes/get.test.ts index 2f7eaea1bc770..7ede2a8b4d7b4 100644 --- a/src/legacy/server/saved_objects/routes/get.test.ts +++ b/src/legacy/server/saved_objects/routes/get.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createGetRoute } from './get'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('GET /api/saved_objects/{type}/{id}', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { savedObjectsClient.get.mockImplementation(() => diff --git a/src/legacy/server/saved_objects/routes/import.test.ts b/src/legacy/server/saved_objects/routes/import.test.ts index 1a0684a35ec79..2b8d9d7523507 100644 --- a/src/legacy/server/saved_objects/routes/import.test.ts +++ b/src/legacy/server/saved_objects/routes/import.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createImportRoute } from './import'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('POST /api/saved_objects/_import', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); const emptyResponse = { saved_objects: [], total: 0, diff --git a/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts b/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts index 7988165207e63..44fa46bccfce5 100644 --- a/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts +++ b/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createResolveImportErrorsRoute } from './resolve_import_errors'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('POST /api/saved_objects/_resolve_import_errors', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { server = createMockServer(); diff --git a/src/legacy/server/saved_objects/routes/update.test.ts b/src/legacy/server/saved_objects/routes/update.test.ts index 69a6fe3030009..aaeaff489d30a 100644 --- a/src/legacy/server/saved_objects/routes/update.test.ts +++ b/src/legacy/server/saved_objects/routes/update.test.ts @@ -20,11 +20,11 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createUpdateRoute } from './update'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; describe('PUT /api/saved_objects/{type}/{id?}', () => { let server: Hapi.Server; - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); beforeEach(() => { const clientResponse = { diff --git a/src/legacy/ui/public/doc_title/__tests__/doc_title.js b/src/legacy/ui/public/doc_title/__tests__/doc_title.js index b4c3700e36f68..fa8b83f755957 100644 --- a/src/legacy/ui/public/doc_title/__tests__/doc_title.js +++ b/src/legacy/ui/public/doc_title/__tests__/doc_title.js @@ -20,7 +20,8 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { setBaseTitle, docTitle } from '../doc_title'; +import { docTitle } from '../doc_title'; +import { npStart } from '../../new_platform'; describe('docTitle Service', function () { let initialDocTitle; @@ -30,20 +31,24 @@ describe('docTitle Service', function () { beforeEach(function () { initialDocTitle = document.title; document.title = MAIN_TITLE; - setBaseTitle(MAIN_TITLE); + npStart.core.chrome.docTitle.__legacy.setBaseTitle(MAIN_TITLE); }); afterEach(function () { document.title = initialDocTitle; - setBaseTitle(initialDocTitle); + npStart.core.chrome.docTitle.__legacy.setBaseTitle(initialDocTitle); }); - beforeEach(ngMock.module('kibana', function ($provide) { - $provide.decorator('$rootScope', decorateWithSpy('$on')); - })); + beforeEach( + ngMock.module('kibana', function ($provide) { + $provide.decorator('$rootScope', decorateWithSpy('$on')); + }) + ); - beforeEach(ngMock.inject(function ($injector) { - $rootScope = $injector.get('$rootScope'); - })); + beforeEach( + ngMock.inject(function ($injector) { + $rootScope = $injector.get('$rootScope'); + }) + ); describe('setup', function () { it('resets the title when a route change begins', function () { @@ -60,13 +65,11 @@ describe('docTitle Service', function () { }); describe('#reset', function () { - it('clears the internal state, next update() will write the default', function () { + it('clears the internal state', function () { docTitle.change('some title'); - docTitle.update(); expect(document.title).to.be('some title - ' + MAIN_TITLE); docTitle.reset(); - docTitle.update(); expect(document.title).to.be(MAIN_TITLE); }); }); @@ -77,12 +80,6 @@ describe('docTitle Service', function () { docTitle.change('some secondary title'); expect(document.title).to.be('some secondary title - ' + MAIN_TITLE); }); - - it('will write just the first param if the second param is true', function () { - expect(document.title).to.be(MAIN_TITLE); - docTitle.change('entire name', true); - expect(document.title).to.be('entire name'); - }); }); function decorateWithSpy(prop) { @@ -91,5 +88,4 @@ describe('docTitle Service', function () { return $delegate; }; } - }); diff --git a/src/legacy/ui/public/doc_title/doc_title.js b/src/legacy/ui/public/doc_title/doc_title.js index 3692fd71f06cc..0edc55a0ac366 100644 --- a/src/legacy/ui/public/doc_title/doc_title.js +++ b/src/legacy/ui/public/doc_title/doc_title.js @@ -17,53 +17,28 @@ * under the License. */ -import _ from 'lodash'; +import { isArray } from 'lodash'; import { uiModules } from '../modules'; +import { npStart } from '../new_platform'; -let baseTitle = document.title; +const npDocTitle = () => npStart.core.chrome.docTitle; -// for karma test -export function setBaseTitle(str) { - baseTitle = str; -} - -let lastChange; - -function render() { - lastChange = lastChange || []; - - const parts = [lastChange[0]]; - - if (!lastChange[1]) parts.push(baseTitle); - - return _(parts).flattenDeep().compact().join(' - '); -} - -function change(title, complete) { - lastChange = [title, complete]; - update(); +function change(title) { + npDocTitle().change(isArray(title) ? title : [title]); } function reset() { - lastChange = null; -} - -function update() { - document.title = render(); + npDocTitle().reset(); } export const docTitle = { - render, change, reset, - update, }; uiModules.get('kibana') .run(function ($rootScope) { // always bind to the route events $rootScope.$on('$routeChangeStart', docTitle.reset); - $rootScope.$on('$routeChangeError', docTitle.update); - $rootScope.$on('$routeChangeSuccess', docTitle.update); }); diff --git a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js index 79365eb5cf1cc..9c4cee6b05db0 100644 --- a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js @@ -20,11 +20,11 @@ import 'ngreact'; import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; -import { TopNavMenu } from '../../../core_plugins/kibana_react/public'; +import { start as navigation } from '../../../core_plugins/navigation/public/legacy'; const module = uiModules.get('kibana'); -module.directive('kbnTopNav', () => { +export function createTopNavDirective() { return { restrict: 'E', template: '', @@ -71,9 +71,12 @@ module.directive('kbnTopNav', () => { return linkFn; } }; -}); +} -module.directive('kbnTopNavHelper', (reactDirective) => { +module.directive('kbnTopNav', createTopNavDirective); + +export function createTopNavHelper(reactDirective) { + const { TopNavMenu } = navigation.ui; return reactDirective( wrapInI18nContext(TopNavMenu), [ @@ -113,4 +116,6 @@ module.directive('kbnTopNavHelper', (reactDirective) => { 'showAutoRefreshOnly', ], ); -}); +} + +module.directive('kbnTopNavHelper', createTopNavHelper); diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 8eac31e24530c..785b0345aa999 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -286,14 +286,12 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( $location: ILocationService, - $rootScope: IRootScopeService, - Private: any, - config: any + $rootScope: IRootScopeService ) => { const urlOverflow = new UrlOverflowService(); const check = () => { // disable long url checks when storing state in session storage - if (config.get('state:storeInSessionStorage')) { + if (newPlatform.uiSettings.get('state:storeInSessionStorage')) { return; } diff --git a/src/legacy/ui/public/modals/confirm_modal.js b/src/legacy/ui/public/modals/confirm_modal.js index 6d5abfca64aaf..9c3f46da4e927 100644 --- a/src/legacy/ui/public/modals/confirm_modal.js +++ b/src/legacy/ui/public/modals/confirm_modal.js @@ -36,16 +36,7 @@ export const ConfirmationButtonTypes = { CANCEL: CANCEL_BUTTON }; -/** - * @typedef {Object} ConfirmModalOptions - * @property {String} confirmButtonText - * @property {String=} cancelButtonText - * @property {function} onConfirm - * @property {function=} onCancel - * @property {String=} title - If given, shows a title on the confirm modal. - */ - -module.factory('confirmModal', function ($rootScope, $compile) { +export function confirmModalFactory($rootScope, $compile) { let modalPopover; const confirmQueue = []; @@ -114,4 +105,15 @@ module.factory('confirmModal', function ($rootScope, $compile) { } } }; -}); +} + +/** + * @typedef {Object} ConfirmModalOptions + * @property {String} confirmButtonText + * @property {String=} cancelButtonText + * @property {function} onConfirm + * @property {function=} onCancel + * @property {String=} title - If given, shows a title on the confirm modal. + */ + +module.factory('confirmModal', confirmModalFactory); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index ae5c0a83bd781..20ea9c9141aca 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -96,6 +96,10 @@ export const npStart = { export function __setup__(coreSetup) { npSetup.core = coreSetup; + + // no-op application register calls (this is overwritten to + // bootstrap an LP plugin outside of tests) + npSetup.core.application.register = () => {}; } export function __start__(coreStart) { diff --git a/src/legacy/ui/public/private/private.js b/src/legacy/ui/public/private/private.js index ef5c59c21dd7a..6257c12ecf696 100644 --- a/src/legacy/ui/public/private/private.js +++ b/src/legacy/ui/public/private/private.js @@ -108,98 +108,100 @@ function name(fn) { return fn.name || fn.toString().split('\n').shift(); } -uiModules.get('kibana/private') - .provider('Private', function () { - const provider = this; - - // one cache/swaps per Provider - const cache = {}; - const swaps = {}; +export function PrivateProvider() { + const provider = this; - // return the uniq id for this function - function identify(fn) { - if (typeof fn !== 'function') { - throw new TypeError('Expected private module "' + fn + '" to be a function'); - } + // one cache/swaps per Provider + const cache = {}; + const swaps = {}; - if (fn.$$id) return fn.$$id; - else return (fn.$$id = nextId()); + // return the uniq id for this function + function identify(fn) { + if (typeof fn !== 'function') { + throw new TypeError('Expected private module "' + fn + '" to be a function'); } - provider.stub = function (fn, instance) { - cache[identify(fn)] = instance; - return instance; - }; + if (fn.$$id) return fn.$$id; + else return (fn.$$id = nextId()); + } - provider.swap = function (fn, prov) { - const id = identify(fn); - swaps[id] = prov; - }; + provider.stub = function (fn, instance) { + cache[identify(fn)] = instance; + return instance; + }; - provider.$get = ['$injector', function PrivateFactory($injector) { + provider.swap = function (fn, prov) { + const id = identify(fn); + swaps[id] = prov; + }; - // prevent circular deps by tracking where we came from - const privPath = []; - const pathToString = function () { - return privPath.map(name).join(' -> '); - }; + provider.$get = ['$injector', function PrivateFactory($injector) { - // call a private provider and return the instance it creates - function instantiate(prov, locals) { - if (~privPath.indexOf(prov)) { - throw new Error( - 'Circular reference to "' + name(prov) + '"' + - ' found while resolving private deps: ' + pathToString() - ); - } + // prevent circular deps by tracking where we came from + const privPath = []; + const pathToString = function () { + return privPath.map(name).join(' -> '); + }; - privPath.push(prov); + // call a private provider and return the instance it creates + function instantiate(prov, locals) { + if (~privPath.indexOf(prov)) { + throw new Error( + 'Circular reference to "' + name(prov) + '"' + + ' found while resolving private deps: ' + pathToString() + ); + } - const context = {}; - let instance = $injector.invoke(prov, context, locals); - if (!_.isObject(instance)) instance = context; + privPath.push(prov); - privPath.pop(); - return instance; - } + const context = {}; + let instance = $injector.invoke(prov, context, locals); + if (!_.isObject(instance)) instance = context; - // retrieve an instance from cache or create and store on - function get(id, prov, $delegateId, $delegateProv) { - if (cache[id]) return cache[id]; + privPath.pop(); + return instance; + } - let instance; + // retrieve an instance from cache or create and store on + function get(id, prov, $delegateId, $delegateProv) { + if (cache[id]) return cache[id]; - if ($delegateId != null && $delegateProv != null) { - instance = instantiate(prov, { - $decorate: _.partial(get, $delegateId, $delegateProv) - }); - } else { - instance = instantiate(prov); - } + let instance; - return (cache[id] = instance); + if ($delegateId != null && $delegateProv != null) { + instance = instantiate(prov, { + $decorate: _.partial(get, $delegateId, $delegateProv) + }); + } else { + instance = instantiate(prov); } - // main api, get the appropriate instance for a provider - function Private(prov) { - let id = identify(prov); - let $delegateId; - let $delegateProv; + return (cache[id] = instance); + } - if (swaps[id]) { - $delegateId = id; - $delegateProv = prov; + // main api, get the appropriate instance for a provider + function Private(prov) { + let id = identify(prov); + let $delegateId; + let $delegateProv; - prov = swaps[$delegateId]; - id = identify(prov); - } + if (swaps[id]) { + $delegateId = id; + $delegateProv = prov; - return get(id, prov, $delegateId, $delegateProv); + prov = swaps[$delegateId]; + id = identify(prov); } - Private.stub = provider.stub; - Private.swap = provider.swap; + return get(id, prov, $delegateId, $delegateProv); + } + + Private.stub = provider.stub; + Private.swap = provider.swap; + + return Private; + }]; +} - return Private; - }]; - }); +uiModules.get('kibana/private') + .provider('Private', PrivateProvider); diff --git a/src/legacy/ui/public/promises/promises.js b/src/legacy/ui/public/promises/promises.js index 99c9a11be7431..af8a5081e0c55 100644 --- a/src/legacy/ui/public/promises/promises.js +++ b/src/legacy/ui/public/promises/promises.js @@ -22,9 +22,7 @@ import { uiModules } from '../modules'; const module = uiModules.get('kibana'); -// Provides a tiny subset of the excellent API from -// bluebird, reimplemented using the $q service -module.service('Promise', function ($q, $timeout) { +export function PromiseServiceCreator($q, $timeout) { function Promise(fn) { if (typeof this === 'undefined') throw new Error('Promise constructor must be called with "new"'); @@ -122,4 +120,8 @@ module.service('Promise', function ($q, $timeout) { }; return Promise; -}); +} + +// Provides a tiny subset of the excellent API from +// bluebird, reimplemented using the $q service +module.service('Promise', PromiseServiceCreator); diff --git a/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx b/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx index 6aea3c72e0c34..3c691c692948a 100644 --- a/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx +++ b/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx @@ -34,7 +34,7 @@ function isSuccess(result: SaveResult): result is { id?: string } { return 'id' in result; } -interface MinimalSaveModalProps { +export interface MinimalSaveModalProps { onSave: (...args: any[]) => Promise; onClose: () => void; } diff --git a/src/legacy/ui/public/state_management/config_provider.js b/src/legacy/ui/public/state_management/config_provider.js index 090210cc8723e..ec770e7fef6ca 100644 --- a/src/legacy/ui/public/state_management/config_provider.js +++ b/src/legacy/ui/public/state_management/config_provider.js @@ -25,21 +25,23 @@ import { uiModules } from '../modules'; -uiModules.get('kibana/state_management') - .provider('stateManagementConfig', class StateManagementConfigProvider { - _enabled = true +export class StateManagementConfigProvider { + _enabled = true + + $get(/* inject stuff */) { + return { + enabled: this._enabled, + }; + } - $get(/* inject stuff */) { - return { - enabled: this._enabled, - }; - } + disable() { + this._enabled = false; + } - disable() { - this._enabled = false; - } + enable() { + this._enabled = true; + } +} - enable() { - this._enabled = true; - } - }); +uiModules.get('kibana/state_management') + .provider('stateManagementConfig', StateManagementConfigProvider); diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap new file mode 100644 index 0000000000000..ab192e6fd3cbb --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NumberList should be rendered with default set of props 1`] = ` + + + + + + + + + + + +`; diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_row.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_row.test.tsx.snap new file mode 100644 index 0000000000000..3882425594cf3 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_row.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NumberRow should be rendered with default set of props 1`] = ` + + + + + + + + +`; diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.test.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.test.tsx new file mode 100644 index 0000000000000..3faf164c365d9 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.test.tsx @@ -0,0 +1,147 @@ +/* + * 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 { shallow } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { NumberList, NumberListProps } from './number_list'; +import { NumberRow } from './number_row'; + +jest.mock('./number_row', () => ({ + NumberRow: () => 'NumberRow', +})); + +jest.mock('@elastic/eui', () => ({ + htmlIdGenerator: jest.fn(() => { + let counter = 1; + return () => `12${counter++}`; + }), + EuiSpacer: require.requireActual('@elastic/eui').EuiSpacer, + EuiFlexItem: require.requireActual('@elastic/eui').EuiFlexItem, + EuiButtonEmpty: require.requireActual('@elastic/eui').EuiButtonEmpty, + EuiFormErrorText: require.requireActual('@elastic/eui').EuiFormErrorText, +})); + +describe('NumberList', () => { + let defaultProps: NumberListProps; + + beforeEach(() => { + defaultProps = { + labelledbyId: 'numberList', + numberArray: [1, 2], + range: '[1, 10]', + showValidation: false, + unitName: 'value', + onChange: jest.fn(), + setTouched: jest.fn(), + setValidity: jest.fn(), + }; + }); + + test('should be rendered with default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + test('should show an order error', () => { + defaultProps.numberArray = [3, 1]; + defaultProps.showValidation = true; + const comp = mountWithIntl(); + + expect(comp.find('EuiFormErrorText').length).toBe(1); + }); + + test('should set validity as true', () => { + mountWithIntl(); + + expect(defaultProps.setValidity).lastCalledWith(true); + }); + + test('should set validity as false when the order is invalid', () => { + defaultProps.numberArray = [3, 2]; + const comp = mountWithIntl(); + + expect(defaultProps.setValidity).lastCalledWith(false); + + comp.setProps({ numberArray: [1, 2] }); + expect(defaultProps.setValidity).lastCalledWith(true); + }); + + test('should set validity as false when there is an empty field', () => { + defaultProps.numberArray = [1, 2]; + const comp = mountWithIntl(); + + comp.setProps({ numberArray: [1, undefined] }); + expect(defaultProps.setValidity).lastCalledWith(false); + }); + + test('should set 0 when number array is empty', () => { + defaultProps.numberArray = []; + mountWithIntl(); + + expect(defaultProps.onChange).lastCalledWith([0]); + }); + + test('should add a number', () => { + const comp = shallow(); + comp.find('EuiButtonEmpty').simulate('click'); + + expect(defaultProps.onChange).lastCalledWith([1, 2, 3]); + }); + + test('should remove a number', () => { + const comp = shallow(); + const row = comp.find(NumberRow).first(); + row.prop('onDelete')(row.prop('model').id); + + expect(defaultProps.onChange).lastCalledWith([2]); + }); + + test('should disable remove button if there is one number', () => { + defaultProps.numberArray = [1]; + const comp = shallow(); + + expect( + comp + .find(NumberRow) + .first() + .prop('disableDelete') + ).toEqual(true); + }); + + test('should change value', () => { + const comp = shallow(); + const row = comp.find(NumberRow).first(); + row.prop('onChange')({ id: row.prop('model').id, value: '3' }); + + expect(defaultProps.onChange).lastCalledWith([3, 2]); + }); + + test('should call setTouched', () => { + const comp = shallow(); + comp + .find(NumberRow) + .first() + .prop('onBlur')(); + + expect(defaultProps.setTouched).toBeCalled(); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.tsx index 54040814028f7..4aae217c1bc8d 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_list.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, useMemo, useCallback } from 'react'; import { EuiSpacer, EuiButtonEmpty, EuiFlexItem, EuiFormErrorText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -34,16 +34,15 @@ import { getUpdatedModels, hasInvalidValues, } from './utils'; +import { useValidation } from '../../agg_utils'; -interface NumberListProps { +export interface NumberListProps { labelledbyId: string; numberArray: Array; range?: string; showValidation: boolean; unitName: string; validateAscendingOrder?: boolean; - onBlur?(): void; - onFocus?(): void; onChange(list: Array): void; setTouched(): void; setValidity(isValid: boolean): void; @@ -56,81 +55,84 @@ function NumberList({ showValidation, unitName, validateAscendingOrder = true, - onBlur, - onFocus, onChange, setTouched, setValidity, }: NumberListProps) { - const numberRange = getRange(range); + const numberRange = useMemo(() => getRange(range), [range]); const [models, setModels] = useState(getInitModelList(numberArray)); const [ascendingError, setAscendingError] = useState(EMPTY_STRING); - // responsible for discarding changes + // set up validity for each model useEffect(() => { - const updatedModels = getUpdatedModels(numberArray, models, numberRange); + let id: number | undefined; if (validateAscendingOrder) { - const isOrderValid = validateOrder(updatedModels); + const { isValidOrder, modelIndex } = validateOrder(numberArray); + id = isValidOrder ? undefined : modelIndex; setAscendingError( - isOrderValid - ? i18n.translate('common.ui.aggTypes.numberList.invalidAscOrderErrorMessage', { + isValidOrder + ? EMPTY_STRING + : i18n.translate('common.ui.aggTypes.numberList.invalidAscOrderErrorMessage', { defaultMessage: 'The values should be in ascending order.', }) - : EMPTY_STRING ); } - setModels(updatedModels); - }, [numberArray]); + setModels(state => getUpdatedModels(numberArray, state, numberRange, id)); + }, [numberArray, numberRange, validateAscendingOrder]); + // responsible for setting up an initial value ([0]) when there is no default value useEffect(() => { - setValidity(!hasInvalidValues(models)); - }, [models]); - - // resposible for setting up an initial value ([0]) when there is no default value - useEffect(() => { - onChange(models.map(({ value }) => (value === EMPTY_STRING ? undefined : value))); + if (!numberArray.length) { + onChange([models[0].value as number]); + } }, []); - const onChangeValue = ({ id, value }: { id: string; value: string }) => { - const parsedValue = parse(value); - const { isValid, errors } = validateValue(parsedValue, numberRange); - setValidity(isValid); + const isValid = !hasInvalidValues(models); + useValidation(setValidity, isValid); - const currentModel = models.find(model => model.id === id); - if (currentModel) { - currentModel.value = parsedValue; - currentModel.isInvalid = !isValid; - currentModel.errors = errors; - } + const onUpdate = useCallback( + (modelList: NumberRowModel[]) => { + setModels(modelList); + onChange(modelList.map(({ value }) => (value === EMPTY_STRING ? undefined : value))); + }, + [onChange] + ); - onUpdate(models); - }; + const onChangeValue = useCallback( + ({ id, value }: { id: string; value: string }) => { + const parsedValue = parse(value); + + onUpdate( + models.map(model => { + if (model.id === id) { + const { isInvalid, error } = validateValue(parsedValue, numberRange); + return { + id, + value: parsedValue, + isInvalid, + error, + }; + } + return model; + }) + ); + }, + [numberRange, models, onUpdate] + ); // Add an item to the end of the list - const onAdd = () => { + const onAdd = useCallback(() => { const newArray = [...models, getNextModel(models, numberRange)]; onUpdate(newArray); - }; - - const onDelete = (id: string) => { - const newArray = models.filter(model => model.id !== id); - onUpdate(newArray); - }; - - const onBlurFn = (model: NumberRowModel) => { - if (model.value === EMPTY_STRING) { - model.isInvalid = true; - } - setTouched(); - if (onBlur) { - onBlur(); - } - }; - - const onUpdate = (modelList: NumberRowModel[]) => { - setModels(modelList); - onChange(modelList.map(({ value }) => (value === EMPTY_STRING ? undefined : value))); - }; + }, [models, numberRange, onUpdate]); + + const onDelete = useCallback( + (id: string) => { + const newArray = models.filter(model => model.id !== id); + onUpdate(newArray); + }, + [models, onUpdate] + ); return ( <> @@ -143,13 +145,12 @@ function NumberList({ labelledbyId={labelledbyId} range={numberRange} onDelete={onDelete} - onFocus={onFocus} onChange={onChangeValue} - onBlur={() => onBlurFn(model)} + onBlur={setTouched} autoFocus={models.length !== 1 && arrayIndex === models.length - 1} /> - {showValidation && model.isInvalid && model.errors && model.errors.length > 0 && ( - {model.errors.join('\n')} + {showValidation && model.isInvalid && model.error && ( + {model.error} )} {models.length - 1 !== arrayIndex && } diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.test.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.test.tsx new file mode 100644 index 0000000000000..2173d92819e3d --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.test.tsx @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NumberRow, NumberRowProps } from './number_row'; + +describe('NumberRow', () => { + let defaultProps: NumberRowProps; + + beforeEach(() => { + defaultProps = { + autoFocus: false, + disableDelete: false, + isInvalid: false, + labelledbyId: 'numberList', + model: { value: 1, id: '1', isInvalid: false }, + range: { + min: 1, + max: 10, + minInclusive: true, + maxInclusive: true, + within: jest.fn(() => true), + }, + onChange: jest.fn(), + onBlur: jest.fn(), + onDelete: jest.fn(), + }; + }); + + test('should be rendered with default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + test('should call onDelete', () => { + const comp = shallow(); + comp.find('EuiButtonIcon').simulate('click'); + + expect(defaultProps.onDelete).lastCalledWith(defaultProps.model.id); + }); + + test('should call onChange', () => { + const comp = shallow(); + comp.find('EuiFieldNumber').prop('onChange')!({ target: { value: '5' } } as React.ChangeEvent< + HTMLInputElement + >); + + expect(defaultProps.onChange).lastCalledWith({ id: defaultProps.model.id, value: '5' }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx index cef574e806e01..23e671180e980 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx @@ -17,13 +17,13 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Range } from '../../../../../../utils/range'; -interface NumberRowProps { +export interface NumberRowProps { autoFocus: boolean; disableDelete: boolean; isInvalid: boolean; @@ -31,7 +31,6 @@ interface NumberRowProps { model: NumberRowModel; range: Range; onBlur(): void; - onFocus?(): void; onChange({ id, value }: { id: string; value: string }): void; onDelete(index: string): void; } @@ -40,7 +39,7 @@ export interface NumberRowModel { id: string; isInvalid: boolean; value: number | ''; - errors?: string[]; + error?: string; } function NumberRow({ @@ -52,7 +51,6 @@ function NumberRow({ range, onBlur, onDelete, - onFocus, onChange, }: NumberRowProps) { const deleteBtnAriaLabel = i18n.translate( @@ -63,11 +61,16 @@ function NumberRow({ } ); - const onValueChanged = (event: React.ChangeEvent) => - onChange({ - value: event.target.value, - id: model.id, - }); + const onValueChanged = useCallback( + (event: React.ChangeEvent) => + onChange({ + value: event.target.value, + id: model.id, + }), + [onChange, model.id] + ); + + const onDeleteFn = useCallback(() => onDelete(model.id), [onDelete, model.id]); return ( @@ -81,7 +84,6 @@ function NumberRow({ defaultMessage: 'Enter a value', })} onChange={onValueChanged} - onFocus={onFocus} value={model.value} fullWidth={true} min={range.min} @@ -95,7 +97,7 @@ function NumberRow({ title={deleteBtnAriaLabel} color="danger" iconType="trash" - onClick={() => onDelete(model.id)} + onClick={onDeleteFn} disabled={disableDelete} /> diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts new file mode 100644 index 0000000000000..3928c0a62eeaa --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts @@ -0,0 +1,218 @@ +/* + * 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 { + getInitModelList, + getUpdatedModels, + validateOrder, + hasInvalidValues, + parse, + validateValue, + getNextModel, + getRange, +} from './utils'; +import { Range } from '../../../../../../utils/range'; +import { NumberRowModel } from './number_row'; + +describe('NumberList utils', () => { + let modelList: NumberRowModel[]; + let range: Range; + + beforeEach(() => { + modelList = [{ value: 1, id: '1', isInvalid: false }, { value: 2, id: '2', isInvalid: false }]; + range = { + min: 1, + max: 10, + minInclusive: true, + maxInclusive: true, + within: jest.fn(() => true), + }; + }); + + describe('getInitModelList', () => { + test('should return list with default model when number list is empty', () => { + const models = getInitModelList([]); + + expect(models).toEqual([{ value: 0, id: expect.any(String), isInvalid: false }]); + }); + + test('should return model list', () => { + const models = getInitModelList([1, undefined]); + + expect(models).toEqual([ + { value: 1, id: expect.any(String), isInvalid: false }, + { value: '', id: expect.any(String), isInvalid: false }, + ]); + }); + }); + + describe('getUpdatedModels', () => { + test('should return model list when number list is empty', () => { + const updatedModelList = getUpdatedModels([], modelList, range); + + expect(updatedModelList).toEqual([{ value: 0, id: expect.any(String), isInvalid: false }]); + }); + + test('should not update model list when number list is the same', () => { + const updatedModelList = getUpdatedModels([1, 2], modelList, range); + + expect(updatedModelList).toEqual(modelList); + }); + + test('should update model list when number list was changed', () => { + const updatedModelList = getUpdatedModels([1, 3], modelList, range); + modelList[1].value = 3; + expect(updatedModelList).toEqual(modelList); + }); + + test('should update model list when number list increased', () => { + const updatedModelList = getUpdatedModels([1, 2, 3], modelList, range); + expect(updatedModelList).toEqual([ + ...modelList, + { value: 3, id: expect.any(String), isInvalid: false }, + ]); + }); + + test('should update model list when number list decreased', () => { + const updatedModelList = getUpdatedModels([2], modelList, range); + expect(updatedModelList).toEqual([{ value: 2, id: '1', isInvalid: false }]); + }); + + test('should update model list when number list has undefined value', () => { + const updatedModelList = getUpdatedModels([1, undefined], modelList, range); + modelList[1].value = ''; + modelList[1].isInvalid = true; + expect(updatedModelList).toEqual(modelList); + }); + + test('should update model list when number order is invalid', () => { + const updatedModelList = getUpdatedModels([1, 3, 2], modelList, range, 2); + expect(updatedModelList).toEqual([ + modelList[0], + { ...modelList[1], value: 3 }, + { value: 2, id: expect.any(String), isInvalid: true }, + ]); + }); + }); + + describe('validateOrder', () => { + test('should return true when order is valid', () => { + expect(validateOrder([1, 2])).toEqual({ + isValidOrder: true, + }); + }); + + test('should return true when a number is undefined', () => { + expect(validateOrder([1, undefined])).toEqual({ + isValidOrder: true, + }); + }); + + test('should return false when order is invalid', () => { + expect(validateOrder([2, 1])).toEqual({ + isValidOrder: false, + modelIndex: 1, + }); + }); + }); + + describe('hasInvalidValues', () => { + test('should return false when there are no invalid models', () => { + expect(hasInvalidValues(modelList)).toBeFalsy(); + }); + + test('should return true when there is an invalid model', () => { + modelList[1].isInvalid = true; + expect(hasInvalidValues(modelList)).toBeTruthy(); + }); + }); + + describe('parse', () => { + test('should return a number', () => { + expect(parse('3')).toBe(3); + }); + + test('should return an empty string when value is invalid', () => { + expect(parse('')).toBe(''); + expect(parse('test')).toBe(''); + expect(parse('NaN')).toBe(''); + }); + }); + + describe('validateValue', () => { + test('should return valid', () => { + expect(validateValue(3, range)).toEqual({ isInvalid: false }); + }); + + test('should return invalid', () => { + range.within = jest.fn(() => false); + expect(validateValue(11, range)).toEqual({ isInvalid: true, error: expect.any(String) }); + }); + }); + + describe('getNextModel', () => { + test('should return 3 as next value', () => { + expect(getNextModel(modelList, range)).toEqual({ + value: 3, + id: expect.any(String), + isInvalid: false, + }); + }); + + test('should return 1 as next value', () => { + expect(getNextModel([{ value: '', id: '2', isInvalid: false }], range)).toEqual({ + value: 1, + id: expect.any(String), + isInvalid: false, + }); + }); + + test('should return 9 as next value', () => { + expect(getNextModel([{ value: 11, id: '2', isInvalid: false }], range)).toEqual({ + value: 9, + id: expect.any(String), + isInvalid: false, + }); + }); + }); + + describe('getRange', () => { + test('should return default range', () => { + expect(getRange()).toEqual({ + min: 0, + max: Infinity, + maxInclusive: false, + minInclusive: true, + }); + }); + + test('should return parsed range', () => { + expect(getRange('(-Infinity, 100]')).toEqual({ + min: -Infinity, + max: 100, + maxInclusive: true, + minInclusive: false, + }); + }); + + test('should throw an error', () => { + expect(() => getRange('test')).toThrowError(); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts index f90f545aeffba..563e8f0a6a9b7 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts @@ -27,6 +27,7 @@ import { NumberRowModel } from './number_row'; const EMPTY_STRING = ''; const defaultRange = parseRange('[0,Infinity)'); const generateId = htmlIdGenerator(); +const defaultModel = { value: 0, id: generateId(), isInvalid: false }; function parse(value: string) { const parsedValue = parseFloat(value); @@ -42,44 +43,37 @@ function getRange(range?: string): Range { } function validateValue(value: number | '', numberRange: Range) { - const result = { - isValid: true, - errors: [] as string[], + const result: { isInvalid: boolean; error?: string } = { + isInvalid: false, }; if (value === EMPTY_STRING) { - result.isValid = false; + result.isInvalid = true; } else if (!numberRange.within(value)) { - result.isValid = false; - result.errors.push( - i18n.translate('common.ui.aggTypes.numberList.invalidRangeErrorMessage', { - defaultMessage: 'The value should be in the range of {min} to {max}.', - values: { min: numberRange.min, max: numberRange.max }, - }) - ); + result.isInvalid = true; + result.error = i18n.translate('common.ui.aggTypes.numberList.invalidRangeErrorMessage', { + defaultMessage: 'The value should be in the range of {min} to {max}.', + values: { min: numberRange.min, max: numberRange.max }, + }); } return result; } -function validateOrder(list: NumberRowModel[]) { - let isInvalidOrder = false; - list.forEach((model, index, array) => { - const previousModel = array[index - 1]; - if (previousModel && model.value !== EMPTY_STRING) { - const isInvalidOrderOfItem = model.value <= previousModel.value; - - if (!model.isInvalid && isInvalidOrderOfItem) { - model.isInvalid = true; - } +function validateOrder(list: Array) { + const result: { isValidOrder: boolean; modelIndex?: number } = { + isValidOrder: true, + }; - if (isInvalidOrderOfItem) { - isInvalidOrder = true; - } + list.forEach((inputValue, index, array) => { + const previousModel = array[index - 1]; + if (previousModel !== undefined && inputValue !== undefined && inputValue <= previousModel) { + result.isValidOrder = false; + result.modelIndex = index; } }); - return isInvalidOrder; + return result; } function getNextModel(list: NumberRowModel[], range: Range): NumberRowModel { @@ -104,26 +98,27 @@ function getInitModelList(list: Array): NumberRowModel[] { id: generateId(), isInvalid: false, })) - : [{ value: 0, id: generateId(), isInvalid: false }]; + : [defaultModel]; } function getUpdatedModels( numberList: Array, modelList: NumberRowModel[], - numberRange: Range + numberRange: Range, + invalidOrderModelIndex?: number ): NumberRowModel[] { if (!numberList.length) { - return modelList; + return [defaultModel]; } return numberList.map((number, index) => { const model = modelList[index] || { id: generateId() }; const newValue: NumberRowModel['value'] = number === undefined ? EMPTY_STRING : number; - const { isValid, errors } = validateValue(newValue, numberRange); + const { isInvalid, error } = validateValue(newValue, numberRange); return { ...model, value: newValue, - isInvalid: !isValid, - errors, + isInvalid: invalidOrderModelIndex === index ? true : isInvalid, + error, }; }); } diff --git a/src/legacy/ui/public/vis/editors/default/controls/percentile_ranks.tsx b/src/legacy/ui/public/vis/editors/default/controls/percentile_ranks.tsx index 8b63bb23475b2..420bf8a87c3d5 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/percentile_ranks.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/percentile_ranks.tsx @@ -37,7 +37,7 @@ function PercentileRanksEditor({ }); const [isValid, setIsValid] = useState(true); - const setModelValidy = (isListValid: boolean) => { + const setModelValidity = (isListValid: boolean) => { setIsValid(isListValid); setValidity(isListValid); }; @@ -62,7 +62,7 @@ function PercentileRanksEditor({ showValidation={showValidation} onChange={setValue} setTouched={setTouched} - setValidity={setModelValidy} + setValidity={setModelValidity} />
); diff --git a/src/legacy/ui/public/vis/editors/default/controls/percentiles.tsx b/src/legacy/ui/public/vis/editors/default/controls/percentiles.tsx index 158ae3442ff0c..b8ad212edead9 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/percentiles.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/percentiles.tsx @@ -37,7 +37,7 @@ function PercentilesEditor({ }); const [isValid, setIsValid] = useState(true); - const setModelValidy = (isListValid: boolean) => { + const setModelValidity = (isListValid: boolean) => { setIsValid(isListValid); setValidity(isListValid); }; @@ -62,7 +62,7 @@ function PercentilesEditor({ showValidation={showValidation} onChange={setValue} setTouched={setTouched} - setValidity={setModelValidy} + setValidity={setModelValidity} /> ); diff --git a/src/legacy/ui/public/vislib/_index.scss b/src/legacy/ui/public/vislib/_index.scss index 4e4ba8175444d..0344fbb5359ec 100644 --- a/src/legacy/ui/public/vislib/_index.scss +++ b/src/legacy/ui/public/vislib/_index.scss @@ -3,3 +3,4 @@ @import './lib/index'; @import './visualizations/point_series/index'; +@import './visualizations/gauges/index'; diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/_index.scss b/src/legacy/ui/public/vislib/visualizations/gauges/_index.scss new file mode 100644 index 0000000000000..1c6b5e669a94b --- /dev/null +++ b/src/legacy/ui/public/vislib/visualizations/gauges/_index.scss @@ -0,0 +1 @@ +@import './meter'; diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/_meter.scss b/src/legacy/ui/public/vislib/visualizations/gauges/_meter.scss new file mode 100644 index 0000000000000..971eaa0eab2f6 --- /dev/null +++ b/src/legacy/ui/public/vislib/visualizations/gauges/_meter.scss @@ -0,0 +1,3 @@ +.visGauge__meter--outline { + stroke: $euiBorderColor; +} diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js index b6a308271497a..7533ae01bc4ac 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js +++ b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js @@ -38,6 +38,7 @@ const defaultConfig = { percentageMode: true, innerSpace: 5, extents: [0, 10000], + outline: false, scale: { show: true, color: '#666', @@ -248,11 +249,13 @@ export class MeterGauge { gauges .append('path') .attr('d', bgArc) + .attr('class', this.gaugeConfig.outline ? 'visGauge__meter--outline' : undefined) .style('fill', this.gaugeConfig.style.bgFill); const series = gauges .append('path') .attr('d', arc) + .attr('class', this.gaugeConfig.outline ? 'visGauge__meter--outline' : undefined) .style('fill', (d) => this.getColorBucket(Math.max(min, d.y))); const smallContainer = svg.node().getBBox().height < 70; diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts index 9f553b37935d7..3da7f83be4894 100644 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts @@ -20,7 +20,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { SavedObjectsClientMock } from '../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request'; // @ts-ignore @@ -129,7 +129,7 @@ describe('uiSettingsMixin()', () => { sinon.assert.notCalled(uiSettingsServiceFactoryStub); - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); decorations.server.uiSettingsServiceFactory({ savedObjectsClient, }); diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index fda25659d1226..374acaaab3999 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -52,5 +52,8 @@ export class UiActionsPlugin implements Plugin return this.api; } - public stop() {} + public stop() { + this.actions.clear(); + this.triggers.clear(); + } } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index 4dc608542c3c3..b30a0e50886d1 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -128,6 +128,8 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { .manage() .window() .setRect({ width, height }); + await driver.executeScript('window.sessionStorage.clear();'); + await driver.executeScript('window.localStorage.clear();'); }); lifecycle.on('cleanup', async () => { diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js b/test/plugin_functional/plugins/kbn_tp_top_nav/index.js new file mode 100644 index 0000000000000..144050beb7868 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_top_nav/index.js @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function (kibana) { + return new kibana.Plugin({ + uiExports: { + app: { + title: 'Top Nav Menu test', + description: 'This is a sample plugin for the functional tests.', + main: 'plugins/kbn_tp_top_nav/app', + } + } + }); +} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json b/test/plugin_functional/plugins/kbn_tp_top_nav/package.json new file mode 100644 index 0000000000000..7102d24d3292d --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_top_nav/package.json @@ -0,0 +1,9 @@ +{ + "name": "kbn_tp_top_nav", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0" +} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js new file mode 100644 index 0000000000000..e7f97e68c086d --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { uiModules } from 'ui/modules'; +import chrome from 'ui/chrome'; + +// This is required so some default styles and required scripts/Angular modules are loaded, +// or the timezone setting is correctly applied. +import 'ui/autoload/all'; + +import { AppWithTopNav } from './top_nav'; + +const app = uiModules.get('apps/topnavDemoPlugin', ['kibana']); + +app.config($locationProvider => { + $locationProvider.html5Mode({ + enabled: false, + requireBase: false, + rewriteLinks: false, + }); +}); + +function RootController($scope, $element) { + const domNode = $element[0]; + + // render react to DOM + render(, domNode); + + // unmount react on controller destroy + $scope.$on('$destroy', () => { + unmountComponentAtNode(domNode); + }); +} + +chrome.setRootController('topnavDemoPlugin', RootController); diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx b/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx new file mode 100644 index 0000000000000..d56ac5f92db88 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx @@ -0,0 +1,54 @@ +/* + * 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, { Component } from 'react'; +import { + setup as navSetup, + start as navStart, +} from '../../../../../src/legacy/core_plugins/navigation/public/legacy'; + +const customExtension = { + id: 'registered-prop', + label: 'Registered Button', + description: 'Registered Demo', + run() {}, + testId: 'demoRegisteredNewButton', +}; + +navSetup.registerMenuItem(customExtension); + +export class AppWithTopNav extends Component { + public render() { + const { TopNavMenu } = navStart.ui; + const config = [ + { + id: 'new', + label: 'New Button', + description: 'New Demo', + run() {}, + testId: 'demoNewButton', + }, + ]; + return ( + + Hey + + ); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json new file mode 100644 index 0000000000000..1ba21f11b7de2 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_top_nav/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/test/plugin_functional/test_suites/core_plugins/index.js b/test/plugin_functional/test_suites/core_plugins/index.js index eeb81f67751ed..376c6f1ebadb1 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.js +++ b/test/plugin_functional/test_suites/core_plugins/index.js @@ -23,5 +23,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./legacy_plugins')); loadTestFile(require.resolve('./server_plugins')); loadTestFile(require.resolve('./ui_plugins')); + loadTestFile(require.resolve('./top_nav')); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/top_nav.js b/test/plugin_functional/test_suites/core_plugins/top_nav.js new file mode 100644 index 0000000000000..5c46e3d7f76db --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/top_nav.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; + +export default function ({ getService, getPageObjects }) { + const PageObjects = getPageObjects(['common']); + + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + describe.skip('top nav', function describeIndexTests() { + before(async () => { + const url = `${PageObjects.common.getHostPort()}/app/kbn_tp_top_nav/`; + await browser.get(url); + }); + + it('Shows registered menu items', async () => { + const ownMenuItem = await testSubjects.find('demoNewButton'); + expect(await ownMenuItem.getVisibleText()).to.be('New Button'); + const demoRegisteredNewButton = await testSubjects.find('demoRegisteredNewButton'); + expect(await demoRegisteredNewButton.getVisibleText()).to.be('Registered Button'); + }); + }); +} diff --git a/test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh new file mode 100755 index 0000000000000..4b16e3b32fefd --- /dev/null +++ b/test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd test/plugin_functional/plugins/kbn_tp_sample_panel_action; +if [[ ! -d "target" ]]; then + checks-reporter-with-killswitch "Build kbn_tp_sample_panel_action" yarn build; +fi +cd -; diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 6deddd5a59152..274cca695b535 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -22,10 +22,7 @@ fi checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; if [ "$CI_GROUP" == "1" ]; then - # build kbn_tp_sample_panel_action - cd test/plugin_functional/plugins/kbn_tp_sample_panel_action; - checks-reporter-with-killswitch "Build kbn_tp_sample_panel_action" yarn build; - cd -; + source test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh yarn run grunt run:pluginFunctionalTestsRelease --from=source; yarn run grunt run:interpreterFunctionalTestsRelease; fi diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy new file mode 100644 index 0000000000000..e8d7cc03edad0 --- /dev/null +++ b/vars/kibanaPipeline.groovy @@ -0,0 +1,251 @@ +def withWorkers(name, preWorkerClosure = {}, workerClosures = [:]) { + return { + jobRunner('tests-xl', true) { + try { + doSetup() + preWorkerClosure() + + def nextWorker = 1 + def worker = { workerClosure -> + def workerNumber = nextWorker + nextWorker++ + + return { + workerClosure(workerNumber) + } + } + + def workers = [:] + workerClosures.each { workerName, workerClosure -> + workers[workerName] = worker(workerClosure) + } + + parallel(workers) + } finally { + catchError { + uploadAllGcsArtifacts(name) + } + + catchError { + runbld.junit() + } + + catchError { + publishJunit() + } + + catchError { + runErrorReporter() + } + } + } + } +} + +def getPostBuildWorker(name, closure) { + return { workerNumber -> + def kibanaPort = "61${workerNumber}1" + def esPort = "61${workerNumber}2" + def esTransportPort = "61${workerNumber}3" + + withEnv([ + "CI_WORKER_NUMBER=${workerNumber}", + "TEST_KIBANA_HOST=localhost", + "TEST_KIBANA_PORT=${kibanaPort}", + "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", + "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", + "TEST_ES_TRANSPORT_PORT=${esTransportPort}", + "IS_PIPELINE_JOB=1", + ]) { + closure() + } + } +} + +def getOssCiGroupWorker(ciGroup) { + return getPostBuildWorker("ciGroup" + ciGroup, { + withEnv([ + "CI_GROUP=${ciGroup}", + "JOB=kibana-ciGroup${ciGroup}", + ]) { + runbld "./test/scripts/jenkins_ci_group.sh" + } + }) +} + +def getXpackCiGroupWorker(ciGroup) { + return getPostBuildWorker("xpack-ciGroup" + ciGroup, { + withEnv([ + "CI_GROUP=${ciGroup}", + "JOB=xpack-kibana-ciGroup${ciGroup}", + ]) { + runbld "./test/scripts/jenkins_xpack_ci_group.sh" + } + }) +} + +def legacyJobRunner(name) { + return { + parallel([ + "${name}": { + withEnv([ + "JOB=${name}", + ]) { + jobRunner('linux && immutable', false) { + try { + runbld('.ci/run.sh', true) + } finally { + catchError { + uploadAllGcsArtifacts(name) + } + catchError { + publishJunit() + } + catchError { + runErrorReporter() + } + } + } + } + } + ]) + } +} + +def jobRunner(label, useRamDisk, closure) { + node(label) { + if (useRamDisk) { + // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm + def originalWorkspace = env.WORKSPACE + ws('/tmp/workspace') { + sh """ + mkdir -p /dev/shm/workspace + mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist + rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it + ln -s /dev/shm/workspace '${originalWorkspace}' + """ + } + } + + def scmVars = checkout scm + + withEnv([ + "CI=true", + "HOME=${env.JENKINS_HOME}", + "PR_SOURCE_BRANCH=${env.ghprbSourceBranch ?: ''}", + "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", + "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", + "TEST_BROWSER_HEADLESS=1", + "GIT_BRANCH=${scmVars.GIT_BRANCH}", + ]) { + withCredentials([ + string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), + string(credentialsId: 'vault-role-id', variable: 'VAULT_ROLE_ID'), + string(credentialsId: 'vault-secret-id', variable: 'VAULT_SECRET_ID'), + ]) { + // scm is configured to check out to the ./kibana directory + dir('kibana') { + closure() + } + } + } + } +} + +// TODO what should happen if GCS, Junit, or email publishing fails? Unstable build? Failed build? + +def uploadGcsArtifact(workerName, pattern) { + def storageLocation = "gs://kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" // TODO + + googleStorageUpload( + credentialsId: 'kibana-ci-gcs-plugin', + bucket: storageLocation, + pattern: pattern, + sharedPublicly: true, + showInline: true, + ) +} + +def uploadAllGcsArtifacts(workerName) { + def ARTIFACT_PATTERNS = [ + 'target/kibana-*', + 'target/junit/**/*', + 'test/**/screenshots/**/*.png', + 'test/functional/failure_debug/html/*.html', + 'x-pack/test/**/screenshots/**/*.png', + 'x-pack/test/functional/failure_debug/html/*.html', + 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', + ] + + ARTIFACT_PATTERNS.each { pattern -> + uploadGcsArtifact(workerName, pattern) + } +} + +def publishJunit() { + junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) +} + +def sendMail() { + // If the build doesn't have a result set by this point, there haven't been any errors and it can be marked as a success + // The e-mail plugin for the infra e-mail depends upon this being set + currentBuild.result = currentBuild.result ?: 'SUCCESS' + + def buildStatus = buildUtils.getBuildStatus() + if (buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { + node('flyweight') { + sendInfraMail() + sendKibanaMail() + } + } +} + +def sendInfraMail() { + catchError { + step([ + $class: 'Mailer', + notifyEveryUnstableBuild: true, + recipients: 'infra-root+build@elastic.co', + sendToIndividuals: false + ]) + } +} + +def sendKibanaMail() { + catchError { + def buildStatus = buildUtils.getBuildStatus() + if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { + emailext( + to: 'build-kibana@elastic.co', + subject: "${env.JOB_NAME} - Build # ${env.BUILD_NUMBER} - ${buildStatus}", + body: '${SCRIPT,template="groovy-html.template"}', + mimeType: 'text/html', + ) + } + } +} + +def bash(script) { + sh "#!/bin/bash\n${script}" +} + +def doSetup() { + runbld "./test/scripts/jenkins_setup.sh" +} + +def buildOss() { + runbld "./test/scripts/jenkins_build_kibana.sh" +} + +def buildXpack() { + runbld "./test/scripts/jenkins_xpack_build_kibana.sh" +} + +def runErrorReporter() { + bash """ + source src/dev/ci_setup/setup_env.sh + node scripts/report_failed_tests + """ +} + +return this diff --git a/vars/runbld.groovy b/vars/runbld.groovy new file mode 100644 index 0000000000000..501e2421ca65b --- /dev/null +++ b/vars/runbld.groovy @@ -0,0 +1,11 @@ +def call(script, enableJunitProcessing = false) { + def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" + + sh "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}" +} + +def junit() { + sh "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh" +} + +return this diff --git a/x-pack/legacy/plugins/actions/server/actions_client.test.ts b/x-pack/legacy/plugins/actions/server/actions_client.test.ts index b582d9f2a1b0d..b4940b23ba61c 100644 --- a/x-pack/legacy/plugins/actions/server/actions_client.test.ts +++ b/x-pack/legacy/plugins/actions/server/actions_client.test.ts @@ -11,9 +11,9 @@ import { ActionsClient } from './actions_client'; import { ExecutorType } from './types'; import { ActionExecutor, TaskRunnerFactory } from './lib'; import { taskManagerMock } from '../../task_manager/task_manager.mock'; -import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); const mockTaskManager = taskManagerMock.create(); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts index 4e2ef29dd740f..8e6b1f19b172c 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts @@ -10,7 +10,7 @@ jest.mock('./lib/send_email', () => ({ import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; import { sendEmail } from './lib/send_email'; import { ActionParamsType, ActionTypeConfigType, ActionTypeSecretsType } from './email'; @@ -23,7 +23,7 @@ const NO_OP_FN = () => {}; const services = { log: NO_OP_FN, callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: SavedObjectsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), }; let actionType: ActionType; diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts index 57a107968ba70..35d81ba74fa72 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -10,7 +10,7 @@ jest.mock('./lib/send_email', () => ({ import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateParams } from '../lib'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; import { ActionParamsType, ActionTypeConfigType } from './es_index'; @@ -20,7 +20,7 @@ const NO_OP_FN = () => {}; const services = { log: NO_OP_FN, callCluster: jest.fn(), - savedObjectsClient: SavedObjectsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), }; let actionType: ActionType; diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts index a9f3ea757e33b..1d453d2bd2340 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -10,7 +10,7 @@ jest.mock('./lib/post_pagerduty', () => ({ import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; @@ -20,7 +20,7 @@ const ACTION_TYPE_ID = '.pagerduty'; const services: Services = { callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: SavedObjectsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), }; let actionType: ActionType; diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts index e3feec6d1bc67..76e639c994834 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -7,7 +7,7 @@ import { ActionType } from '../types'; import { validateParams } from '../lib'; import { Logger } from '../../../../../../src/core/server'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; const ACTION_TYPE_ID = '.server-log'; @@ -92,7 +92,7 @@ describe('execute()', () => { actionId, services: { callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: SavedObjectsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), }, params: { message: 'message text here', level: 'info' }, config: {}, diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts index 681f508b1d214..f6bd2d2b254df 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts @@ -6,7 +6,7 @@ import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; import { ActionTypeRegistry } from '../action_type_registry'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { ActionExecutor, validateParams, validateSecrets, TaskRunnerFactory } from '../lib'; import { getActionType } from './slack'; import { taskManagerMock } from '../../../task_manager/task_manager.mock'; @@ -15,7 +15,7 @@ const ACTION_TYPE_ID = '.slack'; const services: Services = { callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: SavedObjectsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), }; let actionTypeRegistry: ActionTypeRegistry; diff --git a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts index e928eb491c1b5..c6817b3bc12f3 100644 --- a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts @@ -6,10 +6,10 @@ import { taskManagerMock } from '../../task_manager/task_manager.mock'; import { createExecuteFunction } from './create_execute_function'; -import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; const mockTaskManager = taskManagerMock.create(); -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); const getBasePath = jest.fn(); beforeEach(() => jest.resetAllMocks()); diff --git a/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts index 661a08df3dc30..c724717bef9eb 100644 --- a/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts @@ -10,12 +10,12 @@ import { ActionExecutor } from './action_executor'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; import { - SavedObjectsClientMock, + savedObjectsClientMock, loggingServiceMock, } from '../../../../../../src/core/server/mocks'; const actionExecutor = new ActionExecutor(); -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); function getServices() { return { diff --git a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts index cc18c7b169429..6411bc7462c9f 100644 --- a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts @@ -13,7 +13,7 @@ import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { actionExecutorMock } from './action_executor.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; import { - SavedObjectsClientMock, + savedObjectsClientMock, loggingServiceMock, } from '../../../../../../src/core/server/mocks'; @@ -54,7 +54,7 @@ afterAll(() => fakeTimer.restore()); const services = { log: jest.fn(), callCluster: jest.fn(), - savedObjectsClient: SavedObjectsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), }; const actionExecutorInitializerParams = { logger: loggingServiceMock.create().get(), diff --git a/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts b/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts index 340d341a5ef14..7f2341c1aa010 100644 --- a/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts +++ b/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts @@ -5,7 +5,7 @@ */ import Hapi from 'hapi'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { actionsClientMock } from '../actions_client.mock'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; @@ -21,7 +21,7 @@ export function createMockServer(config: Record = defaultConfig) { const actionsClient = actionsClientMock.create(); const actionTypeRegistry = actionTypeRegistryMock.create(); - const savedObjectsClient = SavedObjectsClientMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.create(); server.config = () => { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 574aed3fe9329..093f5f7484004 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -6,13 +6,13 @@ import { schema } from '@kbn/config-schema'; import { AlertsClient } from './alerts_client'; -import { SavedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; const taskManager = taskManagerMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); const alertsClientParams = { taskManager, diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts index ccc91ae2a9034..23591692bca1f 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts @@ -11,7 +11,7 @@ import { ConcreteTaskInstance } from '../../../task_manager'; import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; import { - SavedObjectsClientMock, + savedObjectsClientMock, loggingServiceMock, } from '../../../../../../src/core/server/mocks'; @@ -51,7 +51,7 @@ beforeAll(() => { afterAll(() => fakeTimer.restore()); -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); const services = { log: jest.fn(), diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts index 526bb4e5bcf71..1eeebc8543d72 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts @@ -20,8 +20,8 @@ import { } from '../sections'; export const ERROR_METADATA_SECTIONS: Section[] = [ - ERROR, { ...LABELS, required: true }, + ERROR, HTTP, HOST, CONTAINER, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts index 01e56bdb09f19..7012bbcc8fcea 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts @@ -15,10 +15,10 @@ import { } from '../sections'; export const SPAN_METADATA_SECTIONS: Section[] = [ + LABELS, SPAN, - AGENT, - SERVICE, TRANSACTION, - LABELS, - TRACE + TRACE, + SERVICE, + AGENT ]; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts index 04a0d64077c15..6b30c82bc35a0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts @@ -22,8 +22,8 @@ import { } from '../sections'; export const TRANSACTION_METADATA_SECTIONS: Section[] = [ - TRANSACTION, { ...LABELS, required: true }, + TRANSACTION, HTTP, HOST, CONTAINER, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts index c94b0dcd2bd9d..6efe6bc96dbba 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts @@ -6,8 +6,8 @@ // @ts-ignore import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/constants'; export const EmbeddableTypes = { map: MAP_SAVED_OBJECT_TYPE, diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/legacy/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts index 703ba64b95a7c..a9f41513fbf14 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts @@ -9,14 +9,14 @@ jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') })); import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper'; import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service'; import { createEncryptedSavedObjectsServiceMock } from './encrypted_saved_objects_service.mock'; -import { SavedObjectsClientMock } from 'src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/saved_objects/service/saved_objects_client.mock'; import { SavedObjectsClientContract } from 'src/core/server'; let wrapper: EncryptedSavedObjectsClientWrapper; let mockBaseClient: jest.Mocked; let encryptedSavedObjectsServiceMock: jest.Mocked; beforeEach(() => { - mockBaseClient = SavedObjectsClientMock.create(); + mockBaseClient = savedObjectsClientMock.create(); encryptedSavedObjectsServiceMock = createEncryptedSavedObjectsServiceMock([ { type: 'known-type', diff --git a/x-pack/legacy/plugins/graph/index.js b/x-pack/legacy/plugins/graph/index.js index 1a61caca7a7c1..9ece9966b7da4 100644 --- a/x-pack/legacy/plugins/graph/index.js +++ b/x-pack/legacy/plugins/graph/index.js @@ -23,7 +23,7 @@ export function graph(kibana) { order: 9000, icon: 'plugins/graph/icon.png', euiIconType: 'graphApp', - main: 'plugins/graph/app', + main: 'plugins/graph/index', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'], diff --git a/x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js b/x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js deleted file mode 100644 index 1e6622fd796fb..0000000000000 --- a/x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from '../templates/inspect.html'; -const app = uiModules.get('app/graph'); - -app.directive('graphInspect', function () { - return { - replace: true, - restrict: 'E', - template, - }; -}); diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js index 1d28b4ba5ab30..444e68dd03520 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiModules } from 'ui/modules'; import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; import { i18n } from '@kbn/i18n'; import { @@ -12,8 +11,6 @@ import { injectReferences, } from './saved_workspace_references'; -const module = uiModules.get('app/dashboard'); - export function SavedWorkspaceProvider(Private) { // SavedWorkspace constructor. Usually you'd interact with an instance of this. // ID is option, without it one will be generated on save. @@ -68,8 +65,3 @@ export function SavedWorkspaceProvider(Private) { SavedWorkspace.searchsource = false; return SavedWorkspace; } - -// Used only by the savedDashboards service, usually no reason to change this -module.factory('SavedGraphWorkspace', function (Private) { - return Private(SavedWorkspaceProvider); -}); diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js index 7af0dad3c704c..1fef4b7c38c07 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js @@ -6,7 +6,6 @@ import _ from 'lodash'; -import { uiModules } from 'ui/modules'; import chrome from 'ui/chrome'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; @@ -87,9 +86,5 @@ export function SavedWorkspacesProvider(kbnUrl, Private, Promise) { }); }; } -// This is the only thing that gets injected into controllers -uiModules.get('app/graph').service('savedGraphWorkspaces', function (Private) { - return Private(SavedWorkspacesProvider); -}); SavedObjectRegistryProvider.register(SavedWorkspacesProvider); diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/index.html b/x-pack/legacy/plugins/graph/public/angular/templates/index.html index a3b025ff9d6df..686f0f590a19c 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -4,7 +4,49 @@
- +
+
+ +
+ http://host:port/{{ selectedIndex.name }}/_graph/explore + + +
+
+
+
-
- -
- http://host:port/{{ selectedIndex.name }}/_graph/explore - - -
-
-
-
diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index ba69756e7b070..41e5819bcbf37 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -11,31 +11,10 @@ import React from 'react'; import { Provider } from 'react-redux'; import { isColorDark, hexToRgb } from '@elastic/eui'; -// import the uiExports that we want to "use" -import 'uiExports/fieldFormats'; -import 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; -import 'ui/angular-bootstrap'; -import 'ui/kbn_top_nav'; -import 'ui/directives/saved_object_finder'; -import 'ui/directives/input_focus'; -import 'ui/saved_objects/ui/saved_object_save_as_checkbox'; -import 'uiExports/autocompleteProviders'; -import chrome from 'ui/chrome'; -import { uiModules } from 'ui/modules'; -import uiRoutes from 'ui/routes'; -import { addAppRedirectMessageToUrl, toastNotifications } from 'ui/notify'; -import { formatAngularHttpError } from 'ui/notify/lib'; -import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { npStart } from 'ui/new_platform'; -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import { capabilities } from 'ui/capabilities'; +import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -import { Storage } from 'ui/storage'; - -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { formatAngularHttpError } from 'ui/notify/lib'; +import { addAppRedirectMessageToUrl } from 'ui/notify'; import appTemplate from './angular/templates/index.html'; import listingTemplate from './angular/templates/listing_ng_wrapper.html'; @@ -48,7 +27,6 @@ import { Settings } from './components/settings'; import { GraphVisualization } from './components/graph_visualization'; import gws from './angular/graph_client_workspace.js'; -import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs } from './services/url'; import { createCachedIndexPatternProvider } from './services/index_pattern_cache'; import { urlTemplateRegex } from './helpers/url_template'; @@ -62,528 +40,528 @@ import { hasFieldsSelector } from './state_management'; -import './angular/directives/graph_inspect'; +export function initGraphApp(angularModule, deps) { + const { + xpackInfo, + chrome, + savedGraphWorkspaces, + toastNotifications, + savedObjectsClient, + indexPatterns, + kbnBaseUrl, + addBasePath, + getBasePath, + npData, + config, + savedObjectRegistry, + capabilities, + coreStart, + $http, + Storage, + canEditDrillDownUrls, + graphSavePolicy, + } = deps; + + const app = angularModule; + + function checkLicense(kbnBaseUrl) { + const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && + xpackInfo.get('features.graph.enableAppLink'); + if (!licenseAllowsToShowThisPage) { + const message = xpackInfo.get('features.graph.message'); + const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message); + window.location.href = newUrl; + throw new Error('Graph license error'); + } + } -const app = uiModules.get('app/graph'); + app.directive('vennDiagram', function (reactDirective) { + return reactDirective(VennDiagram); + }); -function checkLicense(kbnBaseUrl) { - const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && - xpackInfo.get('features.graph.enableAppLink'); - if (!licenseAllowsToShowThisPage) { - const message = xpackInfo.get('features.graph.message'); - const newUrl = addAppRedirectMessageToUrl(chrome.addBasePath(kbnBaseUrl), message); - window.location.href = newUrl; - throw new Error('Graph license error'); - } -} + app.directive('graphVisualization', function (reactDirective) { + return reactDirective(GraphVisualization); + }); -app.directive('vennDiagram', function (reactDirective) { - return reactDirective(VennDiagram); -}); - -app.directive('graphListing', function (reactDirective) { - return reactDirective(Listing, [ - ['coreStart', { watchDepth: 'reference' }], - ['createItem', { watchDepth: 'reference' }], - ['findItems', { watchDepth: 'reference' }], - ['deleteItems', { watchDepth: 'reference' }], - ['editItem', { watchDepth: 'reference' }], - ['getViewUrl', { watchDepth: 'reference' }], - ['listingLimit', { watchDepth: 'reference' }], - ['hideWriteControls', { watchDepth: 'reference' }], - ['capabilities', { watchDepth: 'reference' }], - ['initialFilter', { watchDepth: 'reference' }], - ]); -}); - -app.directive('graphApp', function (reactDirective) { - return reactDirective(GraphApp, [ - ['store', { watchDepth: 'reference' }], - ['isInitialized', { watchDepth: 'reference' }], - ['currentIndexPattern', { watchDepth: 'reference' }], - ['indexPatternProvider', { watchDepth: 'reference' }], - ['isLoading', { watchDepth: 'reference' }], - ['onQuerySubmit', { watchDepth: 'reference' }], - ['initialQuery', { watchDepth: 'reference' }], - ['confirmWipeWorkspace', { watchDepth: 'reference' }], - ['coreStart', { watchDepth: 'reference' }], - ['noIndexPatterns', { watchDepth: 'reference' }], - ['reduxStore', { watchDepth: 'reference' }], - ['pluginDataStart', { watchDepth: 'reference' }], - ], { restrict: 'A' }); -}); - -app.directive('graphVisualization', function (reactDirective) { - return reactDirective(GraphVisualization, undefined, { restrict: 'A' }); -}); - -if (uiRoutes.enable) { - uiRoutes.enable(); -} + app.directive('graphListing', function (reactDirective) { + return reactDirective(Listing, [ + ['coreStart', { watchDepth: 'reference' }], + ['createItem', { watchDepth: 'reference' }], + ['findItems', { watchDepth: 'reference' }], + ['deleteItems', { watchDepth: 'reference' }], + ['editItem', { watchDepth: 'reference' }], + ['getViewUrl', { watchDepth: 'reference' }], + ['listingLimit', { watchDepth: 'reference' }], + ['hideWriteControls', { watchDepth: 'reference' }], + ['capabilities', { watchDepth: 'reference' }], + ['initialFilter', { watchDepth: 'reference' }], + ]); + }); -uiRoutes - .when('/home', { - template: listingTemplate, - badge: getReadonlyBadge, - controller($injector, $location, $scope, Private, config, kbnBaseUrl) { - checkLicense(kbnBaseUrl); - const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; - const graphService = services['Graph workspace']; - const kbnUrl = $injector.get('kbnUrl'); + app.directive('graphApp', function (reactDirective) { + return reactDirective(GraphApp, [ + ['store', { watchDepth: 'reference' }], + ['isInitialized', { watchDepth: 'reference' }], + ['currentIndexPattern', { watchDepth: 'reference' }], + ['indexPatternProvider', { watchDepth: 'reference' }], + ['isLoading', { watchDepth: 'reference' }], + ['onQuerySubmit', { watchDepth: 'reference' }], + ['initialQuery', { watchDepth: 'reference' }], + ['confirmWipeWorkspace', { watchDepth: 'reference' }], + ['coreStart', { watchDepth: 'reference' }], + ['noIndexPatterns', { watchDepth: 'reference' }], + ['reduxStore', { watchDepth: 'reference' }], + ['pluginDataStart', { watchDepth: 'reference' }], + ], { restrict: 'A' }); + }); - $scope.listingLimit = config.get('savedObjects:listingLimit'); - $scope.create = () => { - kbnUrl.redirect(getNewPath()); - }; - $scope.find = (search) => { - return graphService.find(search, $scope.listingLimit); - }; - $scope.editItem = (workspace) => { - kbnUrl.redirect(getEditPath(workspace)); - }; - $scope.getViewUrl = (workspace) => getEditUrl(chrome, workspace); - $scope.delete = (workspaces) => { - return graphService.delete(workspaces.map(({ id }) => id)); - }; - $scope.capabilities = capabilities.get().graph; - $scope.initialFilter = ($location.search()).filter || ''; - $scope.coreStart = npStart.core; - setBreadcrumbs({ chrome }); - } - }) - .when('/workspace/:id?', { - template: appTemplate, - badge: getReadonlyBadge, - resolve: { - savedWorkspace: function (savedGraphWorkspaces, $route) { - return $route.current.params.id ? savedGraphWorkspaces.get($route.current.params.id) - .catch( - function () { - toastNotifications.addDanger( - i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: 'Missing workspace', - }) - ); - } - ) : savedGraphWorkspaces.get(); - }, - indexPatterns: function (Private) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - - return savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000 - }).then(response => response.savedObjects); - }, - GetIndexPatternProvider: function (Private) { - return data.indexPatterns.indexPatterns; - }, - SavedWorkspacesProvider: function (Private) { - return Private(SavedWorkspacesProvider); - } - } - }) - .otherwise({ - redirectTo: '/home' + app.directive('graphVisualization', function (reactDirective) { + return reactDirective(GraphVisualization, undefined, { restrict: 'A' }); }); + app.config(function ($routeProvider) { + $routeProvider.when('/home', { + template: listingTemplate, + badge: getReadonlyBadge, + controller($location, $scope) { + checkLicense(kbnBaseUrl); + const services = savedObjectRegistry.byLoaderPropertiesName; + const graphService = services['Graph workspace']; + + $scope.listingLimit = config.get('savedObjects:listingLimit'); + $scope.create = () => { + $location.url(getNewPath()); + }; + $scope.find = (search) => { + return graphService.find(search, $scope.listingLimit); + }; + $scope.editItem = (workspace) => { + $location.url(getEditPath(workspace)); + }; + $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); + $scope.delete = (workspaces) => { + return graphService.delete(workspaces.map(({ id }) => id)); + }; + $scope.capabilities = capabilities; + $scope.initialFilter = ($location.search()).filter || ''; + $scope.coreStart = coreStart; + setBreadcrumbs({ chrome }); + } + }) + .when('/workspace/:id?', { + template: appTemplate, + badge: getReadonlyBadge, + resolve: { + savedWorkspace: function ($route) { + return $route.current.params.id ? savedGraphWorkspaces.get($route.current.params.id) + .catch( + function () { + toastNotifications.addDanger( + i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: 'Missing workspace', + }) + ); + } + ) : savedGraphWorkspaces.get(); + + }, + //Copied from example found in wizard.js ( Kibana TODO - can't + indexPatterns: function () { + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title', 'type'], + perPage: 10000 + }).then(response => response.savedObjects); + }, + GetIndexPatternProvider: function () { + return indexPatterns; + }, + } + }) + .otherwise({ + redirectTo: '/home' + }); + }); -//======== Controller for basic UI ================== -app.controller('graphuiPlugin', function ( - $scope, - $route, - $http, - kbnUrl, - confirmModal, - kbnBaseUrl -) { - checkLicense(kbnBaseUrl); - function handleError(err) { + //======== Controller for basic UI ================== + app.controller('graphuiPlugin', function ($scope, $route, $location, confirmModal) { checkLicense(kbnBaseUrl); - const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { - defaultMessage: 'Graph Error', - description: '"Graph" is a product name and should not be translated.', - }); - if (err instanceof Error) { - toastNotifications.addError(err, { - title: toastTitle, - }); - } else { - toastNotifications.addDanger({ - title: toastTitle, - text: String(err), + + function handleError(err) { + checkLicense(kbnBaseUrl); + const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { + defaultMessage: 'Graph Error', + description: '"Graph" is a product name and should not be translated.', }); + if (err instanceof Error) { + toastNotifications.addError(err, { + title: toastTitle, + }); + } else { + toastNotifications.addDanger({ + title: toastTitle, + text: String(err), + }); + } } - } - async function handleHttpError(error) { - checkLicense(kbnBaseUrl); - toastNotifications.addDanger(formatAngularHttpError(error)); - } + async function handleHttpError(error) { + checkLicense(kbnBaseUrl); + toastNotifications.addDanger(formatAngularHttpError(error)); + } - // Replacement function for graphClientWorkspace's comms so - // that it works with Kibana. - function callNodeProxy(indexName, query, responseHandler) { - const request = { - index: indexName, - query: query - }; - $scope.loading = true; - return $http.post('../api/graph/graphExplore', request) - .then(function (resp) { - if (resp.data.resp.timed_out) { - toastNotifications.addWarning( - i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { - defaultMessage: 'Exploration timed out', - }) - ); - } - responseHandler(resp.data.resp); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - }); - } + // Replacement function for graphClientWorkspace's comms so + // that it works with Kibana. + function callNodeProxy(indexName, query, responseHandler) { + const request = { + index: indexName, + query: query + }; + $scope.loading = true; + return $http.post('../api/graph/graphExplore', request) + .then(function (resp) { + if (resp.data.resp.timed_out) { + toastNotifications.addWarning( + i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { + defaultMessage: 'Exploration timed out', + }) + ); + } + responseHandler(resp.data.resp); + }) + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); + } - //Helper function for the graphClientWorkspace to perform a query - const callSearchNodeProxy = function (indexName, query, responseHandler) { - const request = { - index: indexName, - body: query - }; - $scope.loading = true; - $http.post('../api/graph/searchProxy', request) - .then(function (resp) { - responseHandler(resp.data.resp); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - }); - }; - - $scope.indexPatternProvider = createCachedIndexPatternProvider($route.current.locals.GetIndexPatternProvider.get); - - const store = createGraphStore({ - basePath: chrome.getBasePath(), - indexPatternProvider: $scope.indexPatternProvider, - indexPatterns: $route.current.locals.indexPatterns, - createWorkspace: (indexPattern, exploreControls) => { - const options = { - indexName: indexPattern, - vertex_fields: [], - // Here we have the opportunity to look up labels for nodes... - nodeLabeller: function () { - // console.log(newNodes); - }, - changeHandler: function () { - //Allows DOM to update with graph layout changes. - $scope.$apply(); - }, - graphExploreProxy: callNodeProxy, - searchProxy: callSearchNodeProxy, - exploreControls, + //Helper function for the graphClientWorkspace to perform a query + const callSearchNodeProxy = function (indexName, query, responseHandler) { + const request = { + index: indexName, + body: query }; - $scope.workspace = gws.createWorkspace(options); - }, - setLiveResponseFields: (fields) => { - $scope.liveResponseFields = fields; - }, - setUrlTemplates: (urlTemplates) => { - $scope.urlTemplates = urlTemplates; - }, - getWorkspace: () => { - return $scope.workspace; - }, - getSavedWorkspace: () => { - return $route.current.locals.savedWorkspace; - }, - notifications: npStart.core.notifications, - http: npStart.core.http, - showSaveModal, - setWorkspaceInitialized: () => { - $scope.workspaceInitialized = true; - }, - savePolicy: chrome.getInjected('graphSavePolicy'), - changeUrl: (newUrl) => { - $scope.$evalAsync(() => { - kbnUrl.change(newUrl, {}); - }); - }, - notifyAngular: () => { - $scope.$digest(); - }, - chrome, - }); + $scope.loading = true; + $http.post('../api/graph/searchProxy', request) + .then(function (resp) { + responseHandler(resp.data.resp); + }) + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); + }; - // register things on scope passed down to react components - $scope.pluginDataStart = npStart.plugins.data; - $scope.store = new Storage(window.localStorage); - $scope.coreStart = npStart.core; - $scope.loading = false; - $scope.reduxStore = store; - $scope.savedWorkspace = $route.current.locals.savedWorkspace; - - // register things for legacy angular UI - const allSavingDisabled = chrome.getInjected('graphSavePolicy') === 'none'; - $scope.spymode = 'request'; - $scope.colors = colorChoices; - $scope.isColorDark = (color) => isColorDark(...hexToRgb(color)); - $scope.nodeClick = function (n, $event) { - - //Selection logic - shift key+click helps selects multiple nodes - // Without the shift key we deselect all prior selections (perhaps not - // a great idea for touch devices with no concept of shift key) - if (!$event.shiftKey) { - const prevSelection = n.isSelected; - $scope.workspace.selectNone(); - n.isSelected = prevSelection; - } + $scope.indexPatternProvider = createCachedIndexPatternProvider($route.current.locals.GetIndexPatternProvider.get); + + const store = createGraphStore({ + basePath: getBasePath(), + indexPatternProvider: $scope.indexPatternProvider, + indexPatterns: $route.current.locals.indexPatterns, + createWorkspace: (indexPattern, exploreControls) => { + const options = { + indexName: indexPattern, + vertex_fields: [], + // Here we have the opportunity to look up labels for nodes... + nodeLabeller: function () { + // console.log(newNodes); + }, + changeHandler: function () { + //Allows DOM to update with graph layout changes. + $scope.$apply(); + }, + graphExploreProxy: callNodeProxy, + searchProxy: callSearchNodeProxy, + exploreControls, + }; + $scope.workspace = gws.createWorkspace(options); + }, + setLiveResponseFields: (fields) => { + $scope.liveResponseFields = fields; + }, + setUrlTemplates: (urlTemplates) => { + $scope.urlTemplates = urlTemplates; + }, + getWorkspace: () => { + return $scope.workspace; + }, + getSavedWorkspace: () => { + return $route.current.locals.savedWorkspace; + }, + notifications: coreStart.notifications, + http: coreStart.http, + showSaveModal, + setWorkspaceInitialized: () => { + $scope.workspaceInitialized = true; + }, + savePolicy: graphSavePolicy, + changeUrl: (newUrl) => { + $scope.$evalAsync(() => { + $location.url(newUrl); + }); + }, + notifyAngular: () => { + $scope.$digest(); + }, + chrome, + }); + // register things on scope passed down to react components + $scope.pluginDataStart = npData; + $scope.store = new Storage(window.localStorage); + $scope.coreStart = coreStart; + $scope.loading = false; + $scope.reduxStore = store; + $scope.savedWorkspace = $route.current.locals.savedWorkspace; + + // register things for legacy angular UI + const allSavingDisabled = graphSavePolicy === 'none'; + $scope.spymode = 'request'; + $scope.colors = colorChoices; + $scope.isColorDark = (color) => isColorDark(...hexToRgb(color)); + $scope.nodeClick = function (n, $event) { + + //Selection logic - shift key+click helps selects multiple nodes + // Without the shift key we deselect all prior selections (perhaps not + // a great idea for touch devices with no concept of shift key) + if (!$event.shiftKey) { + const prevSelection = n.isSelected; + $scope.workspace.selectNone(); + n.isSelected = prevSelection; + } - if ($scope.workspace.toggleNodeSelection(n)) { - $scope.selectSelected(n); - } else { - $scope.detail = null; - } - }; - function canWipeWorkspace(callback, text, options) { - if (!hasFieldsSelector(store.getState())) { - callback(); - return; - } - const confirmModalOptions = { - onConfirm: callback, - onCancel: (() => {}), - confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { - defaultMessage: 'Leave anyway', - }), - title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { - defaultMessage: 'Unsaved changes', - }), - ...options, + if ($scope.workspace.toggleNodeSelection(n)) { + $scope.selectSelected(n); + } else { + $scope.detail = null; + } }; - confirmModal(text || i18n.translate('xpack.graph.leaveWorkspace.confirmText', { - defaultMessage: 'If you leave now, you will lose unsaved changes.', - }), confirmModalOptions); - } - $scope.confirmWipeWorkspace = canWipeWorkspace; - $scope.clickEdge = function (edge) { - if (edge.inferred) { - $scope.setDetail ({ 'inferredEdge': edge }); - }else { - $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); - } - }; - - $scope.submit = function (searchTerm) { - $scope.workspaceInitialized = true; - const numHops = 2; - if (searchTerm.startsWith('{')) { - try { - const query = JSON.parse(searchTerm); - if (query.vertices) { - // Is a graph explore request - $scope.workspace.callElasticsearch(query); - }else { - // Is a regular query DSL query - $scope.workspace.search(query, $scope.liveResponseFields, numHops); - } + $scope.clickEdge = function (edge) { + if (edge.inferred) { + $scope.setDetail ({ 'inferredEdge': edge }); + }else { + $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); } - catch (err) { - handleError(err); + }; + + $scope.submit = function (searchTerm) { + $scope.workspaceInitialized = true; + const numHops = 2; + if (searchTerm.startsWith('{')) { + try { + const query = JSON.parse(searchTerm); + if (query.vertices) { + // Is a graph explore request + $scope.workspace.callElasticsearch(query); + }else { + // Is a regular query DSL query + $scope.workspace.search(query, $scope.liveResponseFields, numHops); + } + } + catch (err) { + handleError(err); + } + return; } - return; - } - $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); - }; + $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); + }; - $scope.selectSelected = function (node) { - $scope.detail = { - latestNodeSelection: node + $scope.selectSelected = function (node) { + $scope.detail = { + latestNodeSelection: node + }; + return $scope.selectedSelectedVertex = node; }; - return $scope.selectedSelectedVertex = node; - }; - - $scope.isSelectedSelected = function (node) { - return $scope.selectedSelectedVertex === node; - }; - - $scope.openUrlTemplate = function (template) { - const url = template.url; - const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); - window.open(newUrl, '_blank'); - }; - - $scope.aceLoaded = (editor) => { - editor.$blockScrolling = Infinity; - }; - - $scope.setDetail = function (data) { - $scope.detail = data; - }; - - $scope.performMerge = function (parentId, childId) { - let found = true; - while (found) { - found = false; - for (const i in $scope.detail.mergeCandidates) { - const mc = $scope.detail.mergeCandidates[i]; - if ((mc.id1 === childId) || (mc.id2 === childId)) { - $scope.detail.mergeCandidates.splice(i, 1); - found = true; - break; - } + + $scope.isSelectedSelected = function (node) { + return $scope.selectedSelectedVertex === node; + }; + + $scope.openUrlTemplate = function (template) { + const url = template.url; + const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); + window.open(newUrl, '_blank'); + }; + + $scope.aceLoaded = (editor) => { + editor.$blockScrolling = Infinity; + }; + + $scope.setDetail = function (data) { + $scope.detail = data; + }; + + function canWipeWorkspace(callback, text, options) { + if (!hasFieldsSelector(store.getState())) { + callback(); + return; } + const confirmModalOptions = { + onConfirm: callback, + onCancel: (() => {}), + confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { + defaultMessage: 'Leave anyway', + }), + title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { + defaultMessage: 'Unsaved changes', + }), + ...options, + }; + confirmModal(text || i18n.translate('xpack.graph.leaveWorkspace.confirmText', { + defaultMessage: 'If you leave now, you will lose unsaved changes.', + }), confirmModalOptions); } - $scope.workspace.mergeIds(parentId, childId); - $scope.detail = null; - }; - - - $scope.handleMergeCandidatesCallback = function (termIntersects) { - const mergeCandidates = []; - for (const i in termIntersects) { - const ti = termIntersects[i]; - mergeCandidates.push({ - 'id1': ti.id1, - 'id2': ti.id2, - 'term1': ti.term1, - 'term2': ti.term2, - 'v1': ti.v1, - 'v2': ti.v2, - 'overlap': ti.overlap - }); + $scope.confirmWipeWorkspace = canWipeWorkspace; + + $scope.performMerge = function (parentId, childId) { + let found = true; + while (found) { + found = false; + for (const i in $scope.detail.mergeCandidates) { + const mc = $scope.detail.mergeCandidates[i]; + if ((mc.id1 === childId) || (mc.id2 === childId)) { + $scope.detail.mergeCandidates.splice(i, 1); + found = true; + break; + } + } + } + $scope.workspace.mergeIds(parentId, childId); + $scope.detail = null; + }; - } - $scope.detail = { mergeCandidates }; - }; - - // ===== Menubar configuration ========= - $scope.topNavMenu = []; - $scope.topNavMenu.push({ - key: 'new', - label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { - defaultMessage: 'New', - }), - description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { - defaultMessage: 'New Workspace', - }), - tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { - defaultMessage: 'Create a new workspace', - }), - run: function () { - canWipeWorkspace(function () { - kbnUrl.change('/workspace/', {}); - }); }, - testId: 'graphNewButton', - }); + $scope.handleMergeCandidatesCallback = function (termIntersects) { + const mergeCandidates = []; + for (const i in termIntersects) { + const ti = termIntersects[i]; + mergeCandidates.push({ + 'id1': ti.id1, + 'id2': ti.id2, + 'term1': ti.term1, + 'term2': ti.term2, + 'v1': ti.v1, + 'v2': ti.v2, + 'overlap': ti.overlap + }); - // if saving is disabled using uiCapabilities, we don't want to render the save - // button so it's consistent with all of the other applications - if (capabilities.get().graph.save) { - // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + } + $scope.detail = { mergeCandidates }; + }; + // ===== Menubar configuration ========= + $scope.topNavMenu = []; $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', + key: 'new', + label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { + defaultMessage: 'New', }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save workspace', + description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { + defaultMessage: 'New Workspace', }), - tooltip: () => { - if (allSavingDisabled) { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }); - } else { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', + tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { + defaultMessage: 'Create a new workspace', + }), + run: function () { + canWipeWorkspace(function () { + $scope.$evalAsync(() => { + if ($location.url() === '/workspace/') { + $route.reload(); + } else { + $location.url('/workspace/'); + } }); - } - }, - disableButton: function () { - return allSavingDisabled || !hasFieldsSelector(store.getState()); - }, - run: () => { - store.dispatch({ - type: 'x-pack/graph/SAVE_WORKSPACE', - payload: $route.current.locals.savedWorkspace, }); }, - testId: 'graphSaveButton', + testId: 'graphNewButton', }); - } - $scope.topNavMenu.push({ - key: 'inspect', - disableButton: function () { return $scope.workspace === null; }, - label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { - defaultMessage: 'Inspect', - }), - run: () => { - $scope.$evalAsync(() => { - const curState = $scope.menus.showInspect; - $scope.closeMenus(); - $scope.menus.showInspect = !curState; - }); - }, - }); - $scope.topNavMenu.push({ - key: 'settings', - disableButton: function () { return datasourceSelector(store.getState()).type === 'none'; }, - label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { - defaultMessage: 'Settings', - }), - description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { - defaultMessage: 'Settings', - }), - run: () => { - const settingsObservable = asAngularSyncedObservable(() => ({ - blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, - unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, - canEditDrillDownUrls: chrome.getInjected('canEditDrillDownUrls') - }), $scope.$digest.bind($scope)); - npStart.core.overlays.openFlyout( - - - , { - size: 'm', - closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }), - 'data-test-subj': 'graphSettingsFlyout', - ownFocus: true, - className: 'gphSettingsFlyout', - maxWidth: 520, + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (capabilities.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + + $scope.topNavMenu.push({ + key: 'save', + label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save workspace', + }), + tooltip: () => { + if (allSavingDisabled) { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }); + } else { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }); + } + }, + disableButton: function () { + return allSavingDisabled || !hasFieldsSelector(store.getState()); + }, + run: () => { + store.dispatch({ + type: 'x-pack/graph/SAVE_WORKSPACE', + payload: $route.current.locals.savedWorkspace, + }); + }, + testId: 'graphSaveButton', + }); + } + $scope.topNavMenu.push({ + key: 'inspect', + disableButton: function () { return $scope.workspace === null; }, + label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { + defaultMessage: 'Inspect', + }), + run: () => { + $scope.$evalAsync(() => { + const curState = $scope.menus.showInspect; + $scope.closeMenus(); + $scope.menus.showInspect = !curState; }); - }, - }); - - $scope.menus = { - showSettings: false, - }; - - $scope.closeMenus = () => { - _.forOwn($scope.menus, function (_, key) { - $scope.menus[key] = false; + }, }); - }; - // Deal with situation of request to open saved workspace - if ($route.current.locals.savedWorkspace.id) { - store.dispatch({ - type: 'x-pack/graph/LOAD_WORKSPACE', - payload: $route.current.locals.savedWorkspace, + $scope.topNavMenu.push({ + key: 'settings', + disableButton: function () { return datasourceSelector(store.getState()).type === 'none'; }, + label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { + defaultMessage: 'Settings', + }), + description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { + defaultMessage: 'Settings', + }), + run: () => { + const settingsObservable = asAngularSyncedObservable(() => ({ + blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, + unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, + canEditDrillDownUrls: canEditDrillDownUrls + }), $scope.$digest.bind($scope)); + coreStart.overlays.openFlyout( + + + , { + size: 'm', + closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }), + 'data-test-subj': 'graphSettingsFlyout', + ownFocus: true, + className: 'gphSettingsFlyout', + maxWidth: 520, + }); + }, }); + // Allow URLs to include a user-defined text query if ($route.current.params.query) { $scope.initialQuery = $route.current.params.query; @@ -595,8 +573,26 @@ app.controller('graphuiPlugin', function ( $scope.submit($route.current.params.query); }); } - } else { - $scope.noIndexPatterns = $route.current.locals.indexPatterns.length === 0; - } -}); + $scope.menus = { + showSettings: false, + }; + + $scope.closeMenus = () => { + _.forOwn($scope.menus, function (_, key) { + $scope.menus[key] = false; + }); + }; + + // Deal with situation of request to open saved workspace + if ($route.current.locals.savedWorkspace.id) { + store.dispatch({ + type: 'x-pack/graph/LOAD_WORKSPACE', + payload: $route.current.locals.savedWorkspace, + }); + } else { + $scope.noIndexPatterns = $route.current.locals.indexPatterns.length === 0; + } + }); +//End controller +} diff --git a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js index 11b02354a97c5..5b1eb6181fd61 100644 --- a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiModules } from 'ui/modules'; import { npStart } from 'ui/new_platform'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -uiModules.get('xpack/graph') - .run(() => { - const navLinkUpdates = {}; - navLinkUpdates.hidden = true; - const showAppLink = xpackInfo.get('features.graph.showAppLink', false); - navLinkUpdates.hidden = !showAppLink; - if (showAppLink) { - navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); - navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); - } +const navLinkUpdates = {}; +navLinkUpdates.hidden = true; +const showAppLink = xpackInfo.get('features.graph.showAppLink', false); +navLinkUpdates.hidden = !showAppLink; +if (showAppLink) { + navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); + navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); +} - npStart.core.chrome.navLinks.update('graph', navLinkUpdates); - }); +npStart.core.chrome.navLinks.update('graph', navLinkUpdates); diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts new file mode 100644 index 0000000000000..0249ca74035d6 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// legacy imports currently necessary to power Graph +// for a cutover all of these have to be resolved +import 'uiExports/fieldFormats'; +import 'uiExports/savedObjectTypes'; +import 'uiExports/autocompleteProviders'; +import 'ui/autoload/all'; +import chrome from 'ui/chrome'; +import { IPrivate } from 'ui/private'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { Storage } from 'ui/storage'; +// @ts-ignore +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; + +import { npSetup, npStart } from 'ui/new_platform'; +import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { GraphPlugin } from './plugin'; + +// @ts-ignore +import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; +import { LegacyAngularInjectedDependencies } from './render_app'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularInjectedDependencies(): Promise { + const injector = await chrome.dangerouslyGetActiveInjector(); + + const Private = injector.get('Private'); + + return { + $http: injector.get('$http'), + savedObjectRegistry: Private(SavedObjectRegistryProvider), + kbnBaseUrl: injector.get('kbnBaseUrl'), + savedGraphWorkspaces: Private(SavedWorkspacesProvider), + }; +} + +(async () => { + const instance = new GraphPlugin(); + instance.setup(npSetup.core, { + __LEGACY: { + xpackInfo, + Storage, + }, + }); + instance.start(npStart.core, { + data, + npData: npStart.plugins.data, + __LEGACY: { + angularDependencies: await getAngularInjectedDependencies(), + }, + }); +})(); diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts new file mode 100644 index 0000000000000..d01f325dd8b70 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// NP type imports +import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/core/public'; +import { DataStart } from 'src/legacy/core_plugins/data/public'; +import { Plugin as DataPlugin } from 'src/plugins/data/public'; +import { LegacyAngularInjectedDependencies } from './render_app'; + +export interface GraphPluginStartDependencies { + data: DataStart; + npData: ReturnType; +} + +export interface GraphPluginSetupDependencies { + __LEGACY: { + Storage: any; + xpackInfo: any; + }; +} + +export interface GraphPluginStartDependencies { + __LEGACY: { + angularDependencies: LegacyAngularInjectedDependencies; + }; +} + +export class GraphPlugin implements Plugin { + private dataStart: DataStart | null = null; + private npDataStart: ReturnType | null = null; + private savedObjectsClient: SavedObjectsClientContract | null = null; + private angularDependencies: LegacyAngularInjectedDependencies | null = null; + + setup(core: CoreSetup, { __LEGACY: { xpackInfo, Storage } }: GraphPluginSetupDependencies) { + core.application.register({ + id: 'graph', + title: 'Graph', + mount: async ({ core: contextCore }, params) => { + const { renderApp } = await import('./render_app'); + return renderApp({ + ...params, + npData: this.npDataStart!, + savedObjectsClient: this.savedObjectsClient!, + xpackInfo, + addBasePath: core.http.basePath.prepend, + getBasePath: core.http.basePath.get, + canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( + 'canEditDrillDownUrls' + ) as boolean, + graphSavePolicy: core.injectedMetadata.getInjectedVar('graphSavePolicy') as string, + Storage, + capabilities: contextCore.application.capabilities.graph, + coreStart: contextCore, + chrome: contextCore.chrome, + config: contextCore.uiSettings, + toastNotifications: contextCore.notifications.toasts, + indexPatterns: this.dataStart!.indexPatterns.indexPatterns, + ...this.angularDependencies!, + }); + }, + }); + } + + start( + core: CoreStart, + { data, npData, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies + ) { + // TODO is this really the right way? I though the app context would give us those + this.dataStart = data; + this.npDataStart = npData; + this.angularDependencies = angularDependencies; + this.savedObjectsClient = core.savedObjects.client; + } + + stop() {} +} diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts new file mode 100644 index 0000000000000..8625e20ab9c52 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiConfirmModal } from '@elastic/eui'; + +// inner angular imports +// these are necessary to bootstrap the local angular. +// They can stay even after NP cutover +import angular from 'angular'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import 'ui/angular-bootstrap'; +import 'ace'; +import 'ui/kbn_top_nav'; +import { configureAppAngularModule } from 'ui/legacy_compat'; +// @ts-ignore +import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; +// @ts-ignore +import { confirmModalFactory } from 'ui/modals/confirm_modal'; + +// type imports +import { DataStart } from 'src/legacy/core_plugins/data/public'; +import { + AppMountContext, + ChromeStart, + SavedObjectsClientContract, + ToastsStart, + UiSettingsClientContract, +} from 'kibana/public'; +// @ts-ignore +import { initGraphApp } from './app'; +import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public'; + +/** + * These are dependencies of the Graph app besides the base dependencies + * provided by the application service. Some of those still rely on non-shimmed + * plugins in LP-world, but if they are migrated only the import path in the plugin + * itself changes + */ +export interface GraphDependencies extends LegacyAngularInjectedDependencies { + element: HTMLElement; + appBasePath: string; + capabilities: Record>; + coreStart: AppMountContext['core']; + chrome: ChromeStart; + config: UiSettingsClientContract; + toastNotifications: ToastsStart; + indexPatterns: DataStart['indexPatterns']['indexPatterns']; + npData: ReturnType; + savedObjectsClient: SavedObjectsClientContract; + xpackInfo: { get(path: string): unknown }; + addBasePath: (url: string) => string; + getBasePath: () => string; + Storage: any; + canEditDrillDownUrls: boolean; + graphSavePolicy: string; +} + +/** + * Dependencies of the Graph app which rely on the global angular instance. + * These dependencies have to be migrated to their NP counterparts. + */ +export interface LegacyAngularInjectedDependencies { + /** + * angular $http service + */ + $http: any; + /** + * Instance of SavedObjectRegistryProvider + */ + savedObjectRegistry: any; + kbnBaseUrl: any; + /** + * Instance of SavedWorkspacesProvider + */ + savedGraphWorkspaces: any; +} + +export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { + const graphAngularModule = createLocalAngularModule(deps.coreStart); + configureAppAngularModule(graphAngularModule); + initGraphApp(graphAngularModule, deps); + const $injector = mountGraphApp(appBasePath, element); + return () => $injector.get('$rootScope').$destroy(); +}; + +const mainTemplate = (basePath: string) => `
+ +
+
+`; + +const moduleName = 'app/graph'; + +const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.bootstrap', 'ui.ace']; + +function mountGraphApp(appBasePath: string, element: HTMLElement) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%'); + // eslint-disable-next-line + mountpoint.innerHTML = mainTemplate(appBasePath); + // bootstrap angular into detached element and attach it later to + // make angular-within-angular possible + const $injector = angular.bootstrap(mountpoint, [moduleName]); + element.appendChild(mountpoint); + return $injector; +} + +function createLocalAngularModule(core: AppMountContext['core']) { + createLocalI18nModule(); + createLocalTopNavModule(); + createLocalConfirmModalModule(); + + const graphAngularModule = angular.module(moduleName, [ + ...thirdPartyAngularDependencies, + 'graphI18n', + 'graphTopNav', + 'graphConfirmModal', + ]); + return graphAngularModule; +} + +function createLocalConfirmModalModule() { + angular + .module('graphConfirmModal', ['react']) + .factory('confirmModal', confirmModalFactory) + .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); +} + +function createLocalTopNavModule() { + angular + .module('graphTopNav', ['react']) + .directive('kbnTopNav', createTopNavDirective) + .directive('kbnTopNavHelper', createTopNavHelper); +} + +function createLocalI18nModule() { + angular + .module('graphI18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); +} diff --git a/x-pack/legacy/plugins/graph/public/services/url.ts b/x-pack/legacy/plugins/graph/public/services/url.ts index 91e14564bf3aa..b709683a457fb 100644 --- a/x-pack/legacy/plugins/graph/public/services/url.ts +++ b/x-pack/legacy/plugins/graph/public/services/url.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { Chrome } from 'ui/chrome'; +import { ChromeStart } from 'kibana/public'; import { GraphWorkspaceSavedObject } from '../types'; import { MetaDataState } from '../state_management'; @@ -21,23 +21,26 @@ export function getEditPath({ id }: GraphWorkspaceSavedObject) { return `/workspace/${id}`; } -export function getEditUrl(chrome: Chrome, workspace: GraphWorkspaceSavedObject) { - return chrome.addBasePath(`#${getEditPath(workspace)}`); +export function getEditUrl( + addBasePath: (url: string) => string, + workspace: GraphWorkspaceSavedObject +) { + return addBasePath(`#${getEditPath(workspace)}`); } export type SetBreadcrumbOptions = | { - chrome: Chrome; + chrome: ChromeStart; } | { - chrome: Chrome; + chrome: ChromeStart; metaData: MetaDataState; navigateTo: (path: string) => void; }; export function setBreadcrumbs(options: SetBreadcrumbOptions) { if ('metaData' in options) { - options.chrome.breadcrumbs.set([ + options.chrome.setBreadcrumbs([ { text: i18n.translate('xpack.graph.home.breadcrumb', { defaultMessage: 'Graph', @@ -53,7 +56,7 @@ export function setBreadcrumbs(options: SetBreadcrumbOptions) { }, ]); } else { - options.chrome.breadcrumbs.set([ + options.chrome.setBreadcrumbs([ { text: i18n.translate('xpack.graph.home.breadcrumb', { defaultMessage: 'Graph', diff --git a/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts b/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts index 8ac3746158f3f..25be050edfc13 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts @@ -6,7 +6,6 @@ import { createMockGraphStore, MockedGraphEnvironment } from './mocks'; import { syncBreadcrumbSaga, updateMetaData } from './meta_data'; -import { Chrome } from 'ui/chrome'; describe('breadcrumb sync saga', () => { let env: MockedGraphEnvironment; @@ -14,22 +13,15 @@ describe('breadcrumb sync saga', () => { beforeEach(() => { env = createMockGraphStore({ sagas: [syncBreadcrumbSaga], - mockedDepsOverwrites: { - chrome: ({ - breadcrumbs: { - set: jest.fn(), - }, - } as unknown) as Chrome, - }, }); }); it('syncs breadcrumb initially', () => { - expect(env.mockedDeps.chrome.breadcrumbs.set).toHaveBeenCalled(); + expect(env.mockedDeps.chrome.setBreadcrumbs).toHaveBeenCalled(); }); it('syncs breadcrumb with each change to meta data', () => { env.store.dispatch(updateMetaData({})); - expect(env.mockedDeps.chrome.breadcrumbs.set).toHaveBeenCalledTimes(2); + expect(env.mockedDeps.chrome.setBreadcrumbs).toHaveBeenCalledTimes(2); }); }); diff --git a/x-pack/legacy/plugins/graph/public/state_management/mocks.ts b/x-pack/legacy/plugins/graph/public/state_management/mocks.ts index 11cbf84e759ea..9672edef31d6f 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/mocks.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Chrome } from 'ui/chrome'; import { IndexPattern } from 'src/legacy/core_plugins/data/public'; import { NotificationsStart, HttpStart } from 'kibana/public'; import createSagaMiddleware from 'redux-saga'; import { createStore, applyMiddleware, AnyAction } from 'redux'; +import { ChromeStart } from 'kibana/public'; import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } from './store'; import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; @@ -52,10 +52,8 @@ export function createMockGraphStore({ basePath: 'basepath', changeUrl: jest.fn(), chrome: ({ - breadcrumbs: { - set: jest.fn(), - }, - } as unknown) as Chrome, + setBreadcrumbs: jest.fn(), + } as unknown) as ChromeStart, createWorkspace: jest.fn(), getWorkspace: jest.fn(() => workspaceMock), getSavedWorkspace: jest.fn(() => savedWorkspace), diff --git a/x-pack/legacy/plugins/graph/public/state_management/store.ts b/x-pack/legacy/plugins/graph/public/state_management/store.ts index 752483e65d8cc..bb01f20196f87 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/store.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/store.ts @@ -6,8 +6,8 @@ import createSagaMiddleware, { SagaMiddleware } from 'redux-saga'; import { combineReducers, createStore, Store, AnyAction, Dispatch, applyMiddleware } from 'redux'; +import { ChromeStart } from 'kibana/public'; import { CoreStart } from 'src/core/public'; -import { Chrome } from 'ui/chrome'; import { fieldsReducer, FieldsState, @@ -61,7 +61,7 @@ export interface GraphStoreDependencies { setLiveResponseFields: (fields: WorkspaceField[]) => void; setUrlTemplates: (templates: UrlTemplate[]) => void; setWorkspaceInitialized: () => void; - chrome: Chrome; + chrome: ChromeStart; } export function createRootReducer(basePath: string) { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx index d834b825e414a..8bd9f1074ac54 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx @@ -14,11 +14,13 @@ export const useLogAnalysisResults = ({ startTime, endTime, bucketDuration = 15 * 60 * 1000, + lastRequestTime, }: { sourceId: string; startTime: number; endTime: number; bucketDuration?: number; + lastRequestTime: number; }) => { const { isLoading: isLoadingLogEntryRate, logEntryRate, getLogEntryRate } = useLogEntryRate({ sourceId, @@ -31,7 +33,7 @@ export const useLogAnalysisResults = ({ useEffect(() => { getLogEntryRate(); - }, [sourceId, startTime, endTime, bucketDuration]); + }, [sourceId, startTime, endTime, bucketDuration, lastRequestTime]); return { isLoading, diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx index e846d4e9e4ac5..ffc48a0af9de9 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx @@ -57,9 +57,13 @@ export const AnalysisResultsContent = ({ setAutoRefresh, } = useLogAnalysisResultsUrlState(); - const [queryTimeRange, setQueryTimeRange] = useState( - stringToNumericTimeRange(selectedTimeRange) - ); + const [queryTimeRange, setQueryTimeRange] = useState<{ + value: TimeRange; + lastChangedTime: number; + }>(() => ({ + value: stringToNumericTimeRange(selectedTimeRange), + lastChangedTime: Date.now(), + })); const bucketDuration = useMemo(() => { // This function takes the current time range in ms, @@ -69,18 +73,21 @@ export const AnalysisResultsContent = ({ // 900000 (15 minutes) to it, so that we don't end up with // jaggy bucket boundaries between the ML buckets and our // aggregation buckets. - const msRange = moment(queryTimeRange.endTime).diff(moment(queryTimeRange.startTime)); + const msRange = moment(queryTimeRange.value.endTime).diff( + moment(queryTimeRange.value.startTime) + ); const bucketIntervalInMs = msRange / 100; const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); const roundedResult = parseInt(Number(result).toFixed(0), 10); return roundedResult < bucketSpan ? bucketSpan : roundedResult; - }, [queryTimeRange.startTime, queryTimeRange.endTime]); + }, [queryTimeRange.value.startTime, queryTimeRange.value.endTime]); const { isLoading, logEntryRate } = useLogAnalysisResults({ sourceId, - startTime: queryTimeRange.startTime, - endTime: queryTimeRange.endTime, + startTime: queryTimeRange.value.startTime, + endTime: queryTimeRange.value.endTime, bucketDuration, + lastRequestTime: queryTimeRange.lastChangedTime, }); const hasResults = useMemo(() => logEntryRate && logEntryRate.histogramBuckets.length > 0, [ logEntryRate, @@ -88,7 +95,10 @@ export const AnalysisResultsContent = ({ const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { - setQueryTimeRange(stringToNumericTimeRange({ startTime, endTime })); + setQueryTimeRange({ + value: stringToNumericTimeRange({ startTime, endTime }), + lastChangedTime: Date.now(), + }); }, [setQueryTimeRange] ); @@ -141,6 +151,16 @@ export const AnalysisResultsContent = ({ fetchJobStatus(); }, JOB_STATUS_POLLING_INTERVAL); + useInterval( + () => { + handleQueryTimeRangeChange({ + start: selectedTimeRange.startTime, + end: selectedTimeRange.endTime, + }); + }, + autoRefresh.isPaused ? null : autoRefresh.interval + ); + return ( <> {isLoading && !logEntryRate ? ( @@ -171,9 +191,11 @@ export const AnalysisResultsContent = ({ ), startTime: ( - {moment(queryTimeRange.startTime).format(dateFormat)} + {moment(queryTimeRange.value.startTime).format(dateFormat)} + ), + endTime: ( + {moment(queryTimeRange.value.endTime).format(dateFormat)} ), - endTime: {moment(queryTimeRange.endTime).format(dateFormat)}, }} /> @@ -187,7 +209,6 @@ export const AnalysisResultsContent = ({ isPaused={autoRefresh.isPaused} refreshInterval={autoRefresh.interval} onRefreshChange={handleAutoRefreshChange} - onRefresh={handleQueryTimeRangeChange} /> @@ -200,7 +221,7 @@ export const AnalysisResultsContent = ({ isLoading={isLoading} results={logEntryRate} setTimeRange={handleChartTimeRangeChange} - timeRange={queryTimeRange} + timeRange={queryTimeRange.value} />
@@ -214,7 +235,7 @@ export const AnalysisResultsContent = ({ results={logEntryRate} setTimeRange={handleChartTimeRangeChange} setupStatus={setupStatus} - timeRange={queryTimeRange} + timeRange={queryTimeRange.value} jobId={jobIds['log-entry-rate']} /> diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index a8984bd80fb1c..20f92ebbe0654 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -18,6 +18,7 @@ export const lens: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ id: PLUGIN_ID, configPrefix: `xpack.${PLUGIN_ID}`, + // task_manager could be required, but is only used for telemetry require: ['kibana', 'elasticsearch', 'xpack_main', 'interpreter', 'data'], publicDir: resolve(__dirname, 'public'), diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 0e3e6b0381309..ea4f8edf508e1 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -17,17 +17,22 @@ import { mount } from 'enzyme'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; const dataStartMock = dataPluginMock.createStartContract(); -import { - TopNavMenu, - TopNavMenuData, -} from '../../../../../../src/legacy/core_plugins/kibana_react/public'; +import { TopNavMenuData } from '../../../../../../src/legacy/core_plugins/navigation/public'; import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; import { coreMock } from 'src/core/public/mocks'; -jest.mock('../../../../../../src/legacy/core_plugins/kibana_react/public', () => ({ - TopNavMenu: jest.fn(() => null), +jest.mock('../../../../../../src/legacy/core_plugins/navigation/public/legacy', () => ({ + start: { + ui: { + TopNavMenu: jest.fn(() => null), + }, + }, })); +import { start as navigation } from '../../../../../../src/legacy/core_plugins/navigation/public/legacy'; + +const { TopNavMenu } = navigation.ui; + jest.mock('ui/new_platform'); jest.mock('../persistence'); jest.mock('src/core/public'); diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 1152a3de77181..375ab3aad1eaf 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -20,7 +20,7 @@ import { Query, } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; -import { TopNavMenu } from '../../../../../../src/legacy/core_plugins/kibana_react/public'; +import { start as navigation } from '../../../../../../src/legacy/core_plugins/navigation/public/legacy'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; @@ -163,6 +163,8 @@ export function App({ [] ); + const { TopNavMenu } = navigation.ui; + return ( {}, reuseDomNode: true, - render: async (domNode: Element, config: DatatableProps, _handlers: unknown) => { - ReactDOM.render(, domNode); + render: async ( + domNode: Element, + config: DatatableProps, + handlers: IInterpreterRenderHandlers + ) => { + ReactDOM.render( + , + domNode, + () => { + handlers.done(); + } + ); + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss index 5d181b9c06da8..8f8dc2323b0b7 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss @@ -1,11 +1,12 @@ .lnsExpressionRenderer { + position: relative; width: 100%; height: 100%; display: flex; overflow-x: hidden; padding: $euiSize; -} -.lnsExpressionRenderer > * { - flex: 1; + > .expExpressionRenderer { + position: static; // Let the progress indicator position itself against the outer parent + } } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss index 0e89bbe2da968..cb4ca425afc84 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss @@ -16,6 +16,7 @@ } .lnsSuggestionPanel__button { + position: relative; // Let the expression progress indicator position itself against the button flex: 0 0 auto; width: $lnsSuggestionWidth !important; // sass-lint:disable-line no-important height: $lnsSuggestionHeight; @@ -45,16 +46,22 @@ .lnsSuggestionPanel__chartWrapper { display: flex; - height: $lnsSuggestionHeight - $euiSize; + height: 100%; + width: 100%; pointer-events: none; - margin: 0 $euiSizeS; + padding: $euiSizeS; + + > .expExpressionRenderer { + position: static; // Let the progress indicator position itself against the button + } } .lnsSuggestionPanel__chartWrapper--withLabel { - height: $lnsSuggestionHeight - $euiSizeL; + height: calc(100% - #{$euiSizeL}); } .lnsSuggestionPanel__buttonLabel { + @include euiTextTruncate; @include euiFontSizeXS; display: block; font-weight: $euiFontWeightBold; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss index 7811df93ba8ce..3ef387eca1e8b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss @@ -22,6 +22,7 @@ align-items: stretch; justify-content: stretch; overflow: auto; + position: relative; > * { flex: 1 1 100%; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx index 26db1224eb352..2e95aa10bf4db 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx @@ -52,8 +52,8 @@ function VisualizationSummary(props: Props) { if (!visualization) { return ( <> - {i18n.translate('xpack.lens.configPanel.chooseVisualization', { - defaultMessage: 'Choose a visualization', + {i18n.translate('xpack.lens.configPanel.selectVisualization', { + defaultMessage: 'Select a visualization', })} ); @@ -201,8 +201,8 @@ export function ChartSwitch(props: Props) { anchorPosition="downLeft" > - {i18n.translate('xpack.lens.configPanel.chooseVisualization', { - defaultMessage: 'Choose a visualization', + {i18n.translate('xpack.lens.configPanel.selectVisualization', { + defaultMessage: 'Select a visualization', })} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index 7d1f400f484e1..6aee215d11591 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -63,38 +63,33 @@ const PreviewRenderer = ({ expression: string; ExpressionRendererComponent: ExpressionRenderer; }) => { - const [expressionError, setExpressionError] = useState(false); - - useEffect(() => { - setExpressionError(false); - }, [expression]); - - return expressionError ? ( -
- -
- ) : ( - { - // eslint-disable-next-line no-console - console.error(`Failed to render preview: `, e); - setExpressionError(true); - }} - /> + > + { + return ( +
+ +
+ ); + }} + /> +
); }; @@ -259,20 +254,27 @@ export function SuggestionPanel({ {stagedPreview && ( - { - trackUiEvent('suggestion_confirmed'); - dispatch({ - type: 'SUBMIT_SUGGESTION', - }); - }} - > - {i18n.translate('xpack.lens.sugegstion.confirmSuggestionLabel', { - defaultMessage: 'Reload suggestions', + + > + { + trackUiEvent('suggestion_confirmed'); + dispatch({ + type: 'SUBMIT_SUGGESTION', + }); + }} + > + {i18n.translate('xpack.lens.sugegstion.refreshSuggestionLabel', { + defaultMessage: 'Refresh', + })} + + )} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index ddb82565e4b8b..9cf59f69c0cc2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -392,190 +392,135 @@ describe('workspace_panel', () => { expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); - describe('expression failures', () => { - it('should show an error message if the expression fails to parse', () => { - mockDatasource.toExpression.mockReturnValue('|||'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - /> - ); - - expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); - expect(instance.find(expressionRendererMock)).toHaveLength(0); - }); - - it('should show an error message if the expression fails to render', async () => { - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - expressionRendererMock = jest.fn(({ onRenderFailure }) => { - Promise.resolve().then(() => onRenderFailure!({ type: 'error' })); - return ; - }); - - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - /> - ); - - // "wait" for the expression to execute - await waitForPromises(); + it('should show an error message if the expression fails to parse', () => { + mockDatasource.toExpression.mockReturnValue('|||'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; - instance.update(); + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + /> + ); - expect(instance.find('EuiFlexItem[data-test-subj="expression-failure"]')).toHaveLength(1); - expect(instance.find(expressionRendererMock)).toHaveLength(0); - }); + expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); - it('should not attempt to run the expression again if it does not change', async () => { - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - expressionRendererMock = jest.fn(({ onRenderFailure }) => { - Promise.resolve().then(() => onRenderFailure!({ type: 'error' })); - return ; - }); + it('should not attempt to run the expression again if it does not change', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - /> - ); + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + /> + ); - // "wait" for the expression to execute - await waitForPromises(); + // "wait" for the expression to execute + await waitForPromises(); - instance.update(); + instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(1); - instance.update(); + instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); - }); + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + }); - it('should attempt to run the expression again if changes after an error', async () => { - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - expressionRendererMock = jest.fn(({ onRenderFailure }) => { - Promise.resolve().then(() => onRenderFailure!({ type: 'error' })); - return ; - }); + it('should attempt to run the expression again if it changes', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - /> - ); + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + /> + ); - // "wait" for the expression to execute - await waitForPromises(); + // "wait" for the expression to execute + await waitForPromises(); - instance.update(); + instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(1); - expressionRendererMock.mockImplementation(_ => { - return ; - }); + expressionRendererMock.mockImplementation(_ => { + return ; + }); - instance.setProps({ visualizationState: {} }); - instance.update(); + instance.setProps({ visualizationState: {} }); + instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(2); + expect(expressionRendererMock).toHaveBeenCalledTimes(2); - expect(instance.find(expressionRendererMock)).toHaveLength(1); - }); + expect(instance.find(expressionRendererMock)).toHaveLength(1); }); describe('suggestions from dropping in workspace panel', () => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 1fb1af46f6f0d..d72bf541fd43e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -8,15 +8,14 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { - EuiCodeBlock, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiImage, EuiText, EuiBetaBadge, EuiButtonEmpty, } from '@elastic/eui'; -import { toExpression } from '@kbn/interpreter/common'; import { CoreStart, CoreSetup } from 'src/core/public'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public'; import { Action } from './state_management'; @@ -92,7 +91,10 @@ export function InnerWorkspacePanel({ return suggestions.find(s => s.visualizationId === activeVisualizationId) || suggestions[0]; }, [dragDropContext.dragging]); - const [expressionError, setExpressionError] = useState(undefined); + const [localState, setLocalState] = useState({ + expressionBuildError: undefined as string | undefined, + expandError: false, + }); const activeVisualization = activeVisualizationId ? visualizationMap[activeVisualizationId] @@ -107,7 +109,8 @@ export function InnerWorkspacePanel({ framePublicAPI, }); } catch (e) { - setExpressionError(e.toString()); + // Most likely an error in the expression provided by a datasource or visualization + setLocalState(s => ({ ...s, expressionBuildError: e.toString() })); } }, [ activeVisualization, @@ -178,8 +181,11 @@ export function InnerWorkspacePanel({ function renderVisualization() { useEffect(() => { // reset expression error if component attempts to run it again - if (expressionError) { - setExpressionError(undefined); + if (expression && localState.expressionBuildError) { + setLocalState(s => ({ + ...s, + expressionBuildError: undefined, + })); } }, [expression]); @@ -187,37 +193,63 @@ export function InnerWorkspacePanel({ return renderEmptyWorkspace(); } - if (expressionError) { + if (localState.expressionBuildError) { return ( - + + + + - {/* TODO word this differently because expressions should not be exposed at this level */} - {expression && ( - - {toExpression(expression)} - - )} - - {JSON.stringify(expressionError, null, 2)} - + {localState.expressionBuildError} ); - } else { - return ( + } + + return ( +
{ - setExpressionError(e); + renderError={(errorMessage?: string | null) => { + return ( + + + + + + + + {errorMessage ? ( + + { + setLocalState(prevState => ({ + ...prevState, + expandError: !prevState.expandError, + })); + }} + > + {i18n.translate('xpack.lens.editorFrame.expandRenderingErrorButton', { + defaultMessage: 'Show details of error', + })} + + + {localState.expandError ? errorMessage : null} + + ) : null} + + ); }} /> - ); - } +
+ ); } return ( diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx index 96624764bb8ca..d728457b7e3a3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx @@ -10,7 +10,6 @@ import { Query } from 'src/legacy/core_plugins/data/public'; import { ExpressionRendererProps } from 'src/legacy/core_plugins/expressions/public'; import { Filter } from '@kbn/es-query'; import { Document } from '../../persistence'; -import { act } from 'react-dom/test-utils'; jest.mock('../../../../../../../src/legacy/ui/public/inspector', () => ({ isAvailable: false, @@ -61,25 +60,6 @@ describe('embeddable', () => { expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(savedVis.expression); }); - it('should display error if expression renderering fails', () => { - const embeddable = new Embeddable( - expressionRenderer, - { - editUrl: '', - editable: true, - savedVis, - }, - { id: '123' } - ); - embeddable.render(mountpoint); - - act(() => { - expressionRenderer.mock.calls[0][0]!.onRenderFailure!({ type: 'error' }); - }); - - expect(mountpoint.innerHTML).toContain("Visualization couldn't be displayed"); - }); - it('should re-render if new input is pushed', () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx index daf5e0c8d0dca..bd7588fcfedae 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -31,16 +31,9 @@ export function ExpressionWrapper({ expression, context, }: ExpressionWrapperProps) { - const [expressionError, setExpressionError] = useState(undefined); - useEffect(() => { - // reset expression error if component attempts to run it again - if (expressionError) { - setExpressionError(undefined); - } - }, [expression, context]); return ( - {expression === '' || expressionError ? ( + {expression === '' ? ( @@ -55,14 +48,12 @@ export function ExpressionWrapper({ ) : ( - { - setExpressionError(e); - }} - searchContext={{ ...context, type: 'kibana_context' }} - /> +
+ +
)}
); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx index dc42b97cce08d..4d9bc6276d52a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx @@ -5,8 +5,15 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { EuiButtonEmpty, EuiPopover, EuiSelectable, EuiButtonEmptyProps } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiButtonEmptyProps, +} from '@elastic/eui'; import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; import { IndexPatternRef } from './types'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -62,6 +69,11 @@ export function ChangeIndexPattern({ ownFocus >
+ + {i18n.translate('xpack.lens.indexPattern.changeIndexPatternTitle', { + defaultMessage: 'Change index pattern', + })} + } @@ -441,31 +441,35 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ {paginatedFields.length === 0 && (

- - {showEmptyFields - ? i18n.translate('xpack.lens.indexPatterns.hiddenFieldsLabel', { + {showEmptyFields + ? localState.typeFilter.length || localState.nameFilter.length + ? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', { defaultMessage: - 'No fields have data with the current filters. You can show fields without data using the filters above.', + 'No fields match the current filters. Try changing your filters or time range.', }) : i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', { - defaultMessage: 'No fields in {title} can be visualized.', - values: { title: currentIndexPattern.title }, - })} - + defaultMessage: 'No fields exist in this index pattern.', + }) + : i18n.translate('xpack.lens.indexPatterns.emptyFieldsWithDataLabel', { + defaultMessage: + 'No fields have data with the current filters and time range. Try changing your filters or time range.', + })}

+ {(!showEmptyFields || localState.typeFilter.length || localState.nameFilter.length) && ( { - trackUiEvent('show_empty_fields_clicked'); + trackUiEvent('indexpattern_show_all_fields_clicked'); clearLocalState(); onToggleEmptyFields(true); }} > {i18n.translate('xpack.lens.indexPatterns.showAllFields.buttonText', { - defaultMessage: 'Show All Fields', + defaultMessage: 'Show all fields', })} )} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 41a4bd3549dc1..79a3ac4137f88 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -174,10 +174,8 @@ export function FieldItem(props: FieldItemProps) { togglePopover(); } }} - aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonAriaLabel', { - defaultMessage: - 'Click or press Enter for information about {fieldName}. Or, drag field into visualization.', - values: { fieldName: field.name }, + aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', { + defaultMessage: 'Click for a field preview. Or, drag and drop to visualize.', })} > @@ -188,10 +186,8 @@ export function FieldItem(props: FieldItemProps) { {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: 'No data to display', + defaultMessage: 'No data to display.', })}
); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index 845cfa29d5724..b4f01078f1f78 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -240,7 +240,7 @@ describe('IndexPattern Data Source', () => { columnOrder: ['col1', 'col2'], columns: { col1: { - label: 'Count of Documents', + label: 'Count of records', dataType: 'number', isBucketed: false, @@ -272,7 +272,7 @@ describe('IndexPattern Data Source', () => { metricsAtAllLevels=false partialRows=false includeFormatHints=true - aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of Documents\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}'" + aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}'" `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx index 68a36787ec189..94cb17b340c36 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx @@ -9,7 +9,7 @@ import { OperationDefinition } from '.'; import { ParameterlessIndexPatternColumn, BaseIndexPatternColumn } from './column_types'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { - defaultMessage: 'Count of documents', + defaultMessage: 'Count of records', }); export type CountIndexPatternColumn = ParameterlessIndexPatternColumn< diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx index 89c4899bb8e56..e5c00542df7d3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx @@ -16,7 +16,7 @@ import { IndexPattern } from '../../types'; type PropType = C extends React.ComponentType ? P : unknown; const autoInterval = 'auto'; -const supportedIntervals = ['M', 'w', 'd', 'h']; +const supportedIntervals = ['M', 'w', 'd', 'h', 'm']; const defaultCustomInterval = supportedIntervals[2]; // Add ticks to EuiRange component props diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts index 9b65cbefaf799..6e860c594f4a5 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts @@ -45,7 +45,6 @@ describe('Lens UI telemetry', () => { const fakeManager = new LensReportManager({ http, storage, - basePath: '/basepath', }); setReportManager(fakeManager); }); @@ -84,7 +83,7 @@ describe('Lens UI telemetry', () => { jest.runOnlyPendingTimers(); - expect(http.post).toHaveBeenCalledWith(`/basepath/api/lens/telemetry`, { + expect(http.post).toHaveBeenCalledWith(`/api/lens/telemetry`, { body: JSON.stringify({ events: { '2019-10-23': { diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts index d264e9c77c463..673910deae339 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -45,21 +45,11 @@ export class LensReportManager { private storage: Storage; private http: HttpServiceBase; - private basePath: string; private timer: ReturnType; - constructor({ - storage, - http, - basePath, - }: { - storage: Storage; - http: HttpServiceBase; - basePath: string; - }) { + constructor({ storage, http }: { storage: Storage; http: HttpServiceBase }) { this.storage = storage; this.http = http; - this.basePath = basePath; this.readFromStorage(); @@ -96,7 +86,7 @@ export class LensReportManager { this.readFromStorage(); if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { try { - await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, { + await this.http.post(`${BASE_API_URL}/telemetry`, { body: JSON.stringify({ events: this.events, suggestionEvents: this.suggestionEvents, diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index 68de585771a79..e70a10576106c 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -8,7 +8,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { IInterpreterRenderFunction } from '../../../../../../src/legacy/core_plugins/expressions/public/expressions'; +import { + IInterpreterRenderFunction, + IInterpreterRenderHandlers, +} from '../../../../../../src/legacy/core_plugins/expressions/public/expressions'; import { MetricConfig } from './types'; import { LensMultiTable } from '../types'; import { AutoScale } from './auto_scale'; @@ -76,12 +79,15 @@ export const getMetricChartRenderer = ( formatFactory: FormatFactory ): IInterpreterRenderFunction => ({ name: 'lens_metric_chart_renderer', - displayName: 'Metric Chart', - help: 'Metric Chart Renderer', + displayName: 'Metric chart', + help: 'Metric chart renderer', validate: () => {}, reuseDomNode: true, - render: async (domNode: Element, config: MetricChartProps, _handlers: unknown) => { - ReactDOM.render(, domNode); + render: (domNode: Element, config: MetricChartProps, handlers: IInterpreterRenderHandlers) => { + ReactDOM.render(, domNode, () => { + handlers.done(); + }); + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index de81cc208070a..f81eb9d72d6ae 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -241,7 +241,7 @@ export const visualizationTypes: VisualizationType[] = [ icon: 'visBarHorizontal', largeIcon: chartBarHorizontalSVG, label: i18n.translate('xpack.lens.xyVisualization.barHorizontalLabel', { - defaultMessage: 'Horizontal Bar', + defaultMessage: 'Horizontal bar', }), }, { @@ -249,7 +249,7 @@ export const visualizationTypes: VisualizationType[] = [ icon: 'visBarVerticalStacked', largeIcon: chartBarStackedSVG, label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', { - defaultMessage: 'Stacked Bar', + defaultMessage: 'Stacked bar', }), }, { @@ -257,7 +257,7 @@ export const visualizationTypes: VisualizationType[] = [ icon: 'visBarHorizontalStacked', largeIcon: chartBarHorizontalStackedSVG, label: i18n.translate('xpack.lens.xyVisualization.stackedBarHorizontalLabel', { - defaultMessage: 'Stacked Horizontal Bar', + defaultMessage: 'Stacked horizontal bar', }), }, { @@ -281,7 +281,7 @@ export const visualizationTypes: VisualizationType[] = [ icon: 'visAreaStacked', largeIcon: chartAreaStackedSVG, label: i18n.translate('xpack.lens.xyVisualization.stackedAreaLabel', { - defaultMessage: 'Stacked Area', + defaultMessage: 'Stacked area', }), }, ]; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index f285cb90f225a..f59b1520dbbb4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -20,6 +20,7 @@ import { EuiSpacer, EuiButtonEmpty, EuiPopoverFooter, + EuiToolTip, } from '@elastic/eui'; import { State, SeriesType, LayerConfig, visualizationTypes } from './types'; import { VisualizationProps, OperationMetadata } from '../types'; @@ -235,7 +236,7 @@ export function XYConfigPanel(props: VisualizationProps) { ) { ))} - { - trackUiEvent('xy_layer_added'); - const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType)); - setState({ - ...state, - layers: [ - ...state.layers, - newLayerState( - usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType, - frame.addNewLayer() - ), - ], - }); - }} - iconType="plusInCircleFilled" - /> + + + { + trackUiEvent('xy_layer_added'); + const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType)); + setState({ + ...state, + layers: [ + ...state.layers, + newLayerState( + usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType, + frame.addNewLayer() + ), + ], + }); + }} + iconType="plusInCircleFilled" + /> + + ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 4529179debd26..f55cd8f99a213 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; +import { IInterpreterRenderHandlers } from 'src/legacy/core_plugins/expressions/public'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -98,18 +99,20 @@ export const getXyChartRenderer = (dependencies: { timeZone: string; }): IInterpreterRenderFunction => ({ name: 'lens_xy_chart_renderer', - displayName: 'XY Chart', + displayName: 'XY chart', help: i18n.translate('xpack.lens.xyChart.renderer.help', { - defaultMessage: 'X/Y Chart Renderer', + defaultMessage: 'X/Y chart renderer', }), validate: () => {}, reuseDomNode: true, - render: async (domNode: Element, config: XYChartProps, _handlers: unknown) => { + render: (domNode: Element, config: XYChartProps, handlers: IInterpreterRenderHandlers) => { + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); ReactDOM.render( , - domNode + domNode, + () => handlers.done() ); }, }); @@ -121,16 +124,18 @@ function getIconForSeriesType(seriesType: SeriesType): IconType { const MemoizedChart = React.memo(XYChart); export function XYChartReportable(props: XYChartRenderProps) { - const [isReady, setIsReady] = useState(false); + const [state, setState] = useState({ + isReady: false, + }); // It takes a cycle for the XY chart to render. This prevents // reporting from printing a blank chart placeholder. useEffect(() => { - setIsReady(true); + setState({ isReady: true }); }, []); return ( - + ); @@ -229,7 +234,11 @@ export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderPr }, index ) => { - if (!data.tables[layerId] || data.tables[layerId].rows.length === 0) { + if ( + !data.tables[layerId] || + data.tables[layerId].rows.length === 0 || + data.tables[layerId].rows.every(row => typeof row[xAccessor] === 'undefined') + ) { return; } @@ -238,7 +247,7 @@ export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderPr const rows = data.tables[layerId].rows.map(row => { const newRow: typeof row = {}; - // Remap data to { 'Count of documents': 5 } + // Remap data to { 'Count of records': 5 } Object.keys(row).forEach(key => { if (columnToLabelMap[key]) { newRow[columnToLabelMap[key]] = row[key]; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 4b03c6c346ce5..db28e76f82946 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -47,7 +47,7 @@ describe('xy_visualization', () => { it('should show mixed xy chart when multilple series types', () => { const desc = xyVisualization.getDescription(mixedState('bar', 'line')); - expect(desc.label).toEqual('Mixed XY Chart'); + expect(desc.label).toEqual('Mixed XY chart'); }); it('should show the preferredSeriesType if there are no layers', () => { @@ -56,7 +56,7 @@ describe('xy_visualization', () => { // 'test-file-stub' is a hack, but it at least means we aren't using // a standard icon here. expect(desc.icon).toEqual('test-file-stub'); - expect(desc.label).toEqual('Bar Chart'); + expect(desc.label).toEqual('Bar chart'); }); it('should show mixed horizontal bar chart when multiple horizontal bar types', () => { @@ -64,23 +64,23 @@ describe('xy_visualization', () => { mixedState('bar_horizontal', 'bar_horizontal_stacked') ); - expect(desc.label).toEqual('Mixed Horizontal Bar Chart'); + expect(desc.label).toEqual('Mixed horizontal bar chart'); }); it('should show bar chart when bar only', () => { const desc = xyVisualization.getDescription(mixedState('bar_horizontal', 'bar_horizontal')); - expect(desc.label).toEqual('Horizontal Bar Chart'); + expect(desc.label).toEqual('Horizontal bar chart'); }); it('should show the chart description if not mixed', () => { - expect(xyVisualization.getDescription(mixedState('area')).label).toEqual('Area Chart'); - expect(xyVisualization.getDescription(mixedState('line')).label).toEqual('Line Chart'); + expect(xyVisualization.getDescription(mixedState('area')).label).toEqual('Area chart'); + expect(xyVisualization.getDescription(mixedState('line')).label).toEqual('Line chart'); expect(xyVisualization.getDescription(mixedState('area_stacked')).label).toEqual( - 'Stacked Area Chart' + 'Stacked area chart' ); expect(xyVisualization.getDescription(mixedState('bar_horizontal_stacked')).label).toEqual( - 'Stacked Horizontal Bar Chart' + 'Stacked horizontal bar chart' ); }); }); @@ -119,7 +119,7 @@ describe('xy_visualization', () => { "position": "right", }, "preferredSeriesType": "bar_stacked", - "title": "Empty XY Chart", + "title": "Empty XY chart", } `); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index b56e0d9deb55e..5ba77cb39d5f8 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -54,7 +54,7 @@ function getDescription(state?: State) { ? visualizationType.label : isHorizontalChart(state.layers) ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { - defaultMessage: 'Mixed Horizontal Bar', + defaultMessage: 'Mixed horizontal bar', }) : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { defaultMessage: 'Mixed XY', @@ -70,7 +70,7 @@ export const xyVisualization: Visualization = { getDescription(state) { const { icon, label } = getDescription(state); const chartLabel = i18n.translate('xpack.lens.xyVisualization.chartLabel', { - defaultMessage: '{label} Chart', + defaultMessage: '{label} chart', values: { label }, }); @@ -93,7 +93,7 @@ export const xyVisualization: Visualization = { initialize(frame, state) { return ( state || { - title: 'Empty XY Chart', + title: 'Empty XY chart', legend: { isVisible: true, position: Position.Right }, preferredSeriesType: defaultSeriesType, layers: [ diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 9a58026002ade..94a7c8e0d85c1 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -89,7 +89,11 @@ async function isTaskManagerReady(server: Server) { } async function getLatestTaskState(server: Server) { - const taskManager = server.plugins.task_manager!; + const taskManager = server.plugins.task_manager; + + if (!taskManager) { + return null; + } try { const result = await taskManager.fetch({ diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index 8fc6c8cbefe8a..3cb857a453e1d 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -45,7 +45,13 @@ export function initializeLensTelemetry(core: CoreSetup, { server }: { server: S } function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server }) { - const taskManager = server.plugins.task_manager!; + const taskManager = server.plugins.task_manager; + + if (!taskManager) { + server.log(['debug', 'telemetry'], `Task manager is not available`); + return; + } + taskManager.registerTaskDefinitions({ [TELEMETRY_TASK_TYPE]: { title: 'Lens telemetry fetch task', @@ -62,6 +68,11 @@ function scheduleTasks(server: Server) { status: { plugin: { kbnServer: KbnServer } }; }).status.plugin; + if (!taskManager) { + server.log(['debug', 'telemetry'], `Task manager is not available`); + return; + } + kbnServer.afterPluginsInit(() => { // The code block below can't await directly within "afterPluginsInit" // callback due to circular dependency The server isn't "ready" until @@ -71,14 +82,14 @@ function scheduleTasks(server: Server) { // function block. (async () => { try { - await taskManager!.schedule({ + await taskManager.schedule({ id: TASK_ID, taskType: TELEMETRY_TASK_TYPE, state: { byDate: {}, suggestionsByDate: {}, saved: {}, runs: 0 }, params: {}, }); } catch (e) { - server.log(['warning', 'telemetry'], `Error scheduling task, received ${e.message}`); + server.log(['debug', 'telemetry'], `Error scheduling task, received ${e.message}`); } })(); }); diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 076fae1bb5882..3bb9d48741ab9 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -23,7 +23,8 @@ import _ from 'lodash'; export function maps(kibana) { return new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main', 'tile_map', 'task_manager'], + // task_manager could be required, but is only used for telemetry + require: ['kibana', 'elasticsearch', 'xpack_main', 'tile_map'], id: APP_ID, configPrefix: 'xpack.maps', publicDir: resolve(__dirname, 'public'), diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js index 5f6361a16aa00..c0ac5a781b796 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -9,7 +9,7 @@ import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_ta export function initTelemetryCollection(server) { registerMapsTelemetryTask(server); - scheduleTask(server, server.plugins.task_manager); + scheduleTask(server); registerMapsUsageCollector(server); } @@ -21,6 +21,11 @@ async function isTaskManagerReady(server) { async function fetch(server) { let docs; const taskManager = server.plugins.task_manager; + + if (!taskManager) { + return null; + } + try { ({ docs } = await taskManager.fetch({ query: { diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js index 521c5ae71e14b..3702bc8e29539 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js @@ -10,7 +10,14 @@ const TELEMETRY_TASK_TYPE = 'maps_telemetry'; export const TASK_ID = `Maps-${TELEMETRY_TASK_TYPE}`; -export function scheduleTask(server, taskManager) { +export function scheduleTask(server) { + const taskManager = server.plugins.task_manager; + + if (!taskManager) { + server.log(['debug', 'telemetry'], `Task manager is not available`); + return; + } + const { kbnServer } = server.plugins.xpack_main.status.plugin; kbnServer.afterPluginsInit(() => { @@ -36,6 +43,12 @@ export function scheduleTask(server, taskManager) { export function registerMapsTelemetryTask(server) { const taskManager = server.plugins.task_manager; + + if (!taskManager) { + server.log(['debug', 'telemetry'], `Task manager is not available`); + return; + } + taskManager.registerTaskDefinitions({ [TELEMETRY_TASK_TYPE]: { title: 'Maps telemetry fetch task', diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx index d0e23f96de049..ac7fdcb129531 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx @@ -28,6 +28,7 @@ export const ExpandedRowJsonPane: FC = ({ json }) => { readOnly={true} mode="json" style={{ width: '100%' }} + theme="textmate" />   diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index 9a5344cd1f984..daf21d57b0510 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -127,6 +127,7 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac fontSize: '12px', maxLines: 20, }} + theme="textmate" aria-label={i18n.translate( 'xpack.ml.dataframe.analytics.create.advancedEditor.codeEditorAriaLabel', { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js index 22d45a70075e9..39cd21688dec5 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js @@ -21,6 +21,7 @@ export function MLJobEditor({ mode = EDITOR_MODE.JSON, readOnly = false, syntaxChecking = true, + theme = 'textmate', onChange = () => {} }) { return ( @@ -32,6 +33,7 @@ export function MLJobEditor({ readOnly={readOnly} wrapEnabled={true} showPrintMargin={false} + theme={theme} editorProps={{ $blockScrolling: true }} setOptions={{ useWorker: syntaxChecking, @@ -48,5 +50,7 @@ MLJobEditor.propTypes = { width: PropTypes.string, mode: PropTypes.string, readOnly: PropTypes.bool, + syntaxChecking: PropTypes.bool, + theme: PropTypes.string, onChange: PropTypes.func, }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 6ddfdb22feda8..b9e9df77d35e3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -83,7 +83,7 @@ export const AdvancedDetectorModal: FC = ({ createExcludeFrequentOption(detector.excludeFrequent) ); const [descriptionOption, setDescriptionOption] = useState(detector.description || ''); - const [fieldsEnabled, setFieldsEnabled] = useState(true); + const [splitFieldsEnabled, setSplitFieldsEnabled] = useState(true); const [excludeFrequentEnabled, setExcludeFrequentEnabled] = useState(true); const [fieldOptionEnabled, setFieldOptionEnabled] = useState(true); const { descriptionPlaceholder, setDescriptionPlaceholder } = useDetectorPlaceholder(detector); @@ -139,20 +139,22 @@ export const AdvancedDetectorModal: FC = ({ const partitionField = getField(partitionFieldOption.label); if (agg !== null) { - setFieldsEnabled(true); setCurrentFieldOptions(agg); if (isFieldlessAgg(agg) && eventRateField !== undefined) { + setSplitFieldsEnabled(true); setFieldOption(emptyOption); setFieldOptionEnabled(false); field = eventRateField; } else { + setSplitFieldsEnabled(field !== null); setFieldOptionEnabled(true); - // only enable exclude frequent if there is a by or over selected - setExcludeFrequentEnabled(byField !== null || overField !== null); } + // only enable exclude frequent if there is a by or over selected + setExcludeFrequentEnabled(byField !== null || overField !== null); } else { - setFieldsEnabled(false); + setSplitFieldsEnabled(false); + setFieldOptionEnabled(false); } const dtr: RichDetector = { @@ -179,7 +181,7 @@ export const AdvancedDetectorModal: FC = ({ useEffect(() => { const agg = getAgg(aggOption.label); - setFieldsEnabled(aggOption.label !== ''); + setSplitFieldsEnabled(aggOption.label !== ''); if (agg !== null) { setFieldOptionEnabled(isFieldlessAgg(agg) === false); @@ -202,7 +204,7 @@ export const AdvancedDetectorModal: FC = ({ function saveEnabled() { return ( - fieldsEnabled && + splitFieldsEnabled && (fieldOptionEnabled === false || (fieldOptionEnabled === true && fieldOption.label !== '')) ); } @@ -216,7 +218,7 @@ export const AdvancedDetectorModal: FC = ({ @@ -230,7 +232,7 @@ export const AdvancedDetectorModal: FC = ({ selectedOptions={createSelectedOptions(fieldOption, currentFieldOptions)} onChange={onOptionChange(setFieldOption)} isClearable={true} - isDisabled={fieldsEnabled === false || fieldOptionEnabled === false} + isDisabled={fieldOptionEnabled === false} /> @@ -245,7 +247,7 @@ export const AdvancedDetectorModal: FC = ({ selectedOptions={createSelectedOptions(byFieldOption, splitFieldOptions)} onChange={onOptionChange(setByFieldOption)} isClearable={true} - isDisabled={fieldsEnabled === false} + isDisabled={splitFieldsEnabled === false} /> @@ -257,7 +259,7 @@ export const AdvancedDetectorModal: FC = ({ selectedOptions={createSelectedOptions(overFieldOption, splitFieldOptions)} onChange={onOptionChange(setOverFieldOption)} isClearable={true} - isDisabled={fieldsEnabled === false} + isDisabled={splitFieldsEnabled === false} /> @@ -269,7 +271,7 @@ export const AdvancedDetectorModal: FC = ({ selectedOptions={createSelectedOptions(partitionFieldOption, splitFieldOptions)} onChange={onOptionChange(setPartitionFieldOption)} isClearable={true} - isDisabled={fieldsEnabled === false} + isDisabled={splitFieldsEnabled === false} /> @@ -278,10 +280,13 @@ export const AdvancedDetectorModal: FC = ({ @@ -393,10 +398,13 @@ function createDefaultDescription(dtr: RichDetector) { // if the options list only contains one option and nothing has been selected, set // selectedOptions list to be an empty array function createSelectedOptions( - option: EuiComboBoxOptionProps, + selectedOption: EuiComboBoxOptionProps, options: EuiComboBoxOptionProps[] ): EuiComboBoxOptionProps[] { - return options.length === 1 && options[0].label !== option.label ? [] : [option]; + return (options.length === 1 && options[0].label !== selectedOption.label) || + selectedOption.label === '' + ? [] + : [selectedOption]; } function comboBoxOptionsSort(a: EuiComboBoxOptionProps, b: EuiComboBoxOptionProps) { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx index 2500cf79535d9..d11ef8164169a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx @@ -18,10 +18,12 @@ import { EuiSpacer, EuiCallOut, EuiHorizontalRule, + EuiFormRow, } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; +import { Validation } from '../../../../../common/job_validator'; import { detectorToString } from '../../../../../../../util/string_utils'; interface Props { @@ -31,14 +33,21 @@ interface Props { } export const DetectorList: FC = ({ isActive, onEditJob, onDeleteJob }) => { - const { jobCreator: jc, jobCreatorUpdated } = useContext(JobCreatorContext); + const { jobCreator: jc, jobCreatorUpdated, jobValidator, jobValidatorUpdated } = useContext( + JobCreatorContext + ); const jobCreator = jc as AdvancedJobCreator; const [detectors, setDetectors] = useState(jobCreator.detectors); + const [validation, setValidation] = useState(jobValidator.duplicateDetectors); useEffect(() => { setDetectors(jobCreator.detectors); }, [jobCreatorUpdated]); + useEffect(() => { + setValidation(jobValidator.duplicateDetectors); + }, [jobValidatorUpdated]); + const Buttons: FC<{ index: number }> = ({ index }) => { return ( @@ -99,9 +108,11 @@ export const DetectorList: FC = ({ isActive, onEditJob, onDeleteJob }) => detectorToString(d) )} - - - + {isActive && ( + + + + )} {d.detector_description !== undefined && ( @@ -113,6 +124,7 @@ export const DetectorList: FC = ({ isActive, onEditJob, onDeleteJob }) => ))} + ); }; @@ -140,3 +152,17 @@ const NoDetectorsWarning: FC<{ show: boolean }> = ({ show }) => { ); }; + +const DuplicateDetectorsWarning: FC<{ validation: Validation }> = ({ validation }) => { + if (validation.valid === true) { + return null; + } + return ( + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/oss_telemetry/index.js b/x-pack/legacy/plugins/oss_telemetry/index.js index 01bbd9359783a..eeee9e18f9112 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.js +++ b/x-pack/legacy/plugins/oss_telemetry/index.js @@ -11,7 +11,7 @@ import { PLUGIN_ID } from './constants'; export const ossTelemetry = (kibana) => { return new kibana.Plugin({ id: PLUGIN_ID, - require: ['elasticsearch', 'xpack_main', 'task_manager'], + require: ['elasticsearch', 'xpack_main'], configPrefix: 'xpack.oss_telemetry', init(server) { diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts index 2bbecf99b97c3..63640c87f80a6 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts @@ -14,7 +14,11 @@ async function isTaskManagerReady(server: HapiServer) { } async function fetch(server: HapiServer) { - const taskManager = server.plugins.task_manager!; + const taskManager = server.plugins.task_manager; + + if (!taskManager) { + return null; + } let docs; try { diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts index de3a17a79afb6..eaa8cc7405821 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts @@ -11,6 +11,11 @@ import { visualizationsTaskRunner } from './visualizations/task_runner'; export function registerTasks(server: HapiServer) { const taskManager = server.plugins.task_manager; + if (!taskManager) { + server.log(['debug', 'telemetry'], `Task manager is not available`); + return; + } + taskManager.registerTaskDefinitions({ [VIS_TELEMETRY_TASK]: { title: 'X-Pack telemetry calculator for Visualizations', @@ -43,7 +48,7 @@ export function scheduleTasks(server: HapiServer) { state: { stats: {}, runs: 0 }, }); } catch (e) { - server.log(['warning', 'telemetry'], `Error scheduling task, received ${e.message}`); + server.log(['debug', 'telemetry'], `Error scheduling task, received ${e.message}`); } })(); }); diff --git a/x-pack/legacy/plugins/reporting/common/cancellation_token.ts b/x-pack/legacy/plugins/reporting/common/cancellation_token.ts index 11425c3002d97..c03f9ee7328cb 100644 --- a/x-pack/legacy/plugins/reporting/common/cancellation_token.ts +++ b/x-pack/legacy/plugins/reporting/common/cancellation_token.ts @@ -7,11 +7,11 @@ import { isFunction } from 'lodash'; export class CancellationToken { - private isCancelled: boolean; + private _isCancelled: boolean; private _callbacks: Function[]; constructor() { - this.isCancelled = false; + this._isCancelled = false; this._callbacks = []; } @@ -20,7 +20,7 @@ export class CancellationToken { throw new Error('Expected callback to be a function'); } - if (this.isCancelled) { + if (this._isCancelled) { callback(); return; } @@ -29,7 +29,11 @@ export class CancellationToken { }; public cancel = () => { - this.isCancelled = true; + this._isCancelled = true; this._callbacks.forEach(callback => callback()); }; + + public isCancelled() { + return this._isCancelled; + } } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts new file mode 100644 index 0000000000000..c439c2bbf60eb --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import sinon from 'sinon'; +import { CancellationToken } from '../../../../../common/cancellation_token'; +import { Logger, ScrollConfig } from '../../../../../types'; +import { createHitIterator } from '../hit_iterator'; + +const mockLogger = { + error: new Function(), + debug: new Function(), + warning: new Function(), +} as Logger; +const debugLogStub = sinon.stub(mockLogger, 'debug'); +const warnLogStub = sinon.stub(mockLogger, 'warning'); +const errorLogStub = sinon.stub(mockLogger, 'error'); +const mockCallEndpoint = sinon.stub(); +const mockSearchRequest = {}; +const mockConfig: ScrollConfig = { duration: '2s', size: 123 }; +let realCancellationToken = new CancellationToken(); +let isCancelledStub: sinon.SinonStub; + +describe('hitIterator', function() { + beforeEach(() => { + debugLogStub.resetHistory(); + warnLogStub.resetHistory(); + errorLogStub.resetHistory(); + mockCallEndpoint.resetHistory(); + mockCallEndpoint.resetBehavior(); + mockCallEndpoint.resolves({ _scroll_id: '123blah', hits: { hits: ['you found me'] } }); + mockCallEndpoint.onCall(11).resolves({ _scroll_id: '123blah', hits: {} }); + + isCancelledStub = sinon.stub(realCancellationToken, 'isCancelled'); + isCancelledStub.returns(false); + }); + + afterEach(() => { + realCancellationToken = new CancellationToken(); + }); + + it('iterates hits', async () => { + // Begin + const hitIterator = createHitIterator(mockLogger); + const iterator = hitIterator( + mockConfig, + mockCallEndpoint, + mockSearchRequest, + realCancellationToken + ); + + while (true) { + const { done: iterationDone, value: hit } = await iterator.next(); + if (iterationDone) { + break; + } + expect(hit).to.be('you found me'); + } + + expect(mockCallEndpoint.callCount).to.be(13); + expect(debugLogStub.callCount).to.be(13); + expect(warnLogStub.callCount).to.be(0); + expect(errorLogStub.callCount).to.be(0); + }); + + it('stops searches after cancellation', async () => { + // Setup + isCancelledStub.onFirstCall().returns(false); + isCancelledStub.returns(true); + + // Begin + const hitIterator = createHitIterator(mockLogger); + const iterator = hitIterator( + mockConfig, + mockCallEndpoint, + mockSearchRequest, + realCancellationToken + ); + + while (true) { + const { done: iterationDone, value: hit } = await iterator.next(); + if (iterationDone) { + break; + } + expect(hit).to.be('you found me'); + } + + expect(mockCallEndpoint.callCount).to.be(3); + expect(debugLogStub.callCount).to.be(3); + expect(warnLogStub.callCount).to.be(1); + expect(errorLogStub.callCount).to.be(0); + + expect(warnLogStub.firstCall.lastArg).to.be( + 'Any remaining scrolling searches have been cancelled by the cancellation token.' + ); + }); + + it('handles time out', async () => { + // Setup + mockCallEndpoint.onCall(2).resolves({ status: 404 }); + + // Begin + const hitIterator = createHitIterator(mockLogger); + const iterator = hitIterator( + mockConfig, + mockCallEndpoint, + mockSearchRequest, + realCancellationToken + ); + + let errorThrown = false; + try { + while (true) { + const { done: iterationDone, value: hit } = await iterator.next(); + if (iterationDone) { + break; + } + expect(hit).to.be('you found me'); + } + } catch (err) { + expect(err).to.eql( + new Error('Expected _scroll_id in the following Elasticsearch response: {"status":404}') + ); + errorThrown = true; + } + + expect(mockCallEndpoint.callCount).to.be(4); + expect(debugLogStub.callCount).to.be(4); + expect(warnLogStub.callCount).to.be(0); + expect(errorLogStub.callCount).to.be(1); + expect(errorThrown).to.be(true); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.js deleted file mode 100644 index 5ad6182568721..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -async function parseResponse(request) { - const response = await request; - if (!response._scroll_id) { - throw new Error(i18n.translate('xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage', { - defaultMessage: 'Expected {scrollId} in the following Elasticsearch response: {response}', - values: { response: JSON.stringify(response), scrollId: '_scroll_id' } - })); - } - - if (!response.hits) { - throw new Error(i18n.translate('xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage', { - defaultMessage: 'Expected {hits} in the following Elasticsearch response: {response}', - values: { response: JSON.stringify(response), hits: 'hits' } - })); - } - - return { - scrollId: response._scroll_id, - hits: response.hits.hits - }; -} - -export function createHitIterator(logger) { - return async function* hitIterator(scrollSettings, callEndpoint, searchRequest, cancellationToken) { - logger.debug('executing search request'); - function search(index, body) { - return parseResponse(callEndpoint('search', { - index, - body, - scroll: scrollSettings.duration, - size: scrollSettings.size - })); - } - - function scroll(scrollId) { - logger.debug('executing scroll request'); - return parseResponse(callEndpoint('scroll', { - scrollId, - scroll: scrollSettings.duration - })); - } - - function clearScroll(scrollId) { - logger.debug('executing clearScroll request'); - return callEndpoint('clearScroll', { - scrollId: [ scrollId ] - }); - } - - let { scrollId, hits } = await search(searchRequest.index, searchRequest.body); - try { - while(hits.length && !cancellationToken.isCancelled) { - for(const hit of hits) { - yield hit; - } - - ({ scrollId, hits } = await scroll(scrollId)); - } - } finally { - await clearScroll(scrollId); - } - }; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts new file mode 100644 index 0000000000000..68836c01369e3 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SearchParams, SearchResponse } from 'elasticsearch'; + +import { i18n } from '@kbn/i18n'; +import { CancellationToken, ScrollConfig, Logger } from '../../../../types'; + +async function parseResponse(request: SearchResponse) { + const response = await request; + if (!response || !response._scroll_id) { + throw new Error( + i18n.translate('xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage', { + defaultMessage: 'Expected {scrollId} in the following Elasticsearch response: {response}', + values: { response: JSON.stringify(response), scrollId: '_scroll_id' }, + }) + ); + } + + if (!response.hits) { + throw new Error( + i18n.translate('xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage', { + defaultMessage: 'Expected {hits} in the following Elasticsearch response: {response}', + values: { response: JSON.stringify(response), hits: 'hits' }, + }) + ); + } + + return { + scrollId: response._scroll_id, + hits: response.hits.hits, + }; +} + +export function createHitIterator(logger: Logger) { + return async function* hitIterator( + scrollSettings: ScrollConfig, + callEndpoint: Function, + searchRequest: SearchParams, + cancellationToken: CancellationToken + ) { + logger.debug('executing search request'); + function search(index: string | boolean | string[] | undefined, body: object) { + return parseResponse( + callEndpoint('search', { + index, + body, + scroll: scrollSettings.duration, + size: scrollSettings.size, + }) + ); + } + + function scroll(scrollId: string | undefined) { + logger.debug('executing scroll request'); + return parseResponse( + callEndpoint('scroll', { + scrollId, + scroll: scrollSettings.duration, + }) + ); + } + + function clearScroll(scrollId: string | undefined) { + logger.debug('executing clearScroll request'); + return callEndpoint('clearScroll', { + scrollId: [scrollId], + }); + } + + try { + let { scrollId, hits } = await search(searchRequest.index, searchRequest.body); + try { + while (hits && hits.length && !cancellationToken.isCancelled()) { + for (const hit of hits) { + yield hit; + } + + ({ scrollId, hits } = await scroll(scrollId)); + + if (cancellationToken.isCancelled()) { + logger.warning( + 'Any remaining scrolling searches have been cancelled by the cancellation token.' + ); + } + } + } finally { + await clearScroll(scrollId); + } + } catch (err) { + logger.error(err); + throw err; + } + }; +} diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 62f8286cdf364..731d0f084a718 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -112,6 +112,11 @@ export interface QueueConfig { timeout: number; } +export interface ScrollConfig { + duration: string; + size: number; +} + export interface ElementPosition { boundingClientRect: { // modern browsers support x/y, but older ones don't @@ -248,5 +253,7 @@ export interface ExportTypesRegistry { register: (exportTypeDefinition: ExportTypeDefinition) => void; } +export { CancellationToken } from './common/cancellation_token'; + // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger } from './server/lib/level_logger'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index 11b604571378b..c513f7a451240 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -5,7 +5,7 @@ */ import { defaultTo, noop } from 'lodash/fp'; -import * as React from 'react'; +import React, { useCallback } from 'react'; import { DragDropContext, DropResult, DragStart } from 'react-beautiful-dnd'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; @@ -59,22 +59,25 @@ const onDragEndHandler = ({ */ export const DragDropContextWrapperComponent = React.memo( ({ browserFields, children, dataProviders, dispatch }) => { - function onDragEnd(result: DropResult) { - enableScrolling(); - - if (dataProviders != null) { - onDragEndHandler({ - browserFields, - result, - dataProviders, - dispatch, - }); - } - - if (!draggableIsField(result)) { - document.body.classList.remove(IS_DRAGGING_CLASS_NAME); - } - } + const onDragEnd = useCallback( + (result: DropResult) => { + enableScrolling(); + + if (dataProviders != null) { + onDragEndHandler({ + browserFields, + result, + dataProviders, + dispatch, + }); + } + + if (!draggableIsField(result)) { + document.body.classList.remove(IS_DRAGGING_CLASS_NAME); + } + }, + [browserFields, dataProviders] + ); return ( {children} diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index dc7f2185c26b7..ec6646fc76085 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -17,7 +17,7 @@ import { EuiSpacer, EuiToolTip, } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -101,7 +101,7 @@ export const StatefulEditDataProvider = React.memo( const [updatedValue, setUpdatedValue] = useState(value); /** Focuses the Value input if it is visible, falling back to the Save button if it's not */ - function focusInput() { + const focusInput = () => { const elements = document.getElementsByClassName(VALUE_INPUT_CLASS_NAME); if (elements.length > 0) { @@ -113,25 +113,25 @@ export const StatefulEditDataProvider = React.memo( (saveElements[0] as HTMLElement).focus(); } } - } + }; - function onFieldSelected(selectedField: EuiComboBoxOptionProps[]) { + const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionProps[]) => { setUpdatedField(selectedField); focusInput(); - } + }, []); - function onOperatorSelected(operatorSelected: EuiComboBoxOptionProps[]) { + const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionProps[]) => { setUpdatedOperator(operatorSelected); focusInput(); - } + }, []); - function onValueChange(e: React.ChangeEvent) { + const onValueChange = useCallback((e: React.ChangeEvent) => { setUpdatedValue(e.target.value); - } + }, []); - function disableScrolling() { + const disableScrolling = () => { const x = window.pageXOffset !== undefined ? window.pageXOffset @@ -143,11 +143,11 @@ export const StatefulEditDataProvider = React.memo( : (document.documentElement || document.body.parentNode || document.body).scrollTop; window.onscroll = () => window.scrollTo(x, y); - } + }; - function enableScrolling() { + const enableScrolling = () => { window.onscroll = () => noop; - } + }; useEffect(() => { disableScrolling(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap new file mode 100644 index 0000000000000..f343316d88c46 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Embeddable it renders 1`] = ` + + +

+ Test content +

+
+
+`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap new file mode 100644 index 0000000000000..e88693b292a5d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmbeddableHeader it renders 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap index 1f23686adca0e..bf0dfd9417875 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap @@ -1,19 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EmbeddedMap renders correctly against snapshot 1`] = ` - + + + + + Map configuration help + + + } > - + - - - + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index 1aa3b087ac0d0..fb896059460b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -15,43 +15,41 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` body={

- An ECS compliant Kibana index pattern must be configured to view event data on the map. When using beats, you can run the following setup commands to create the required Kibana index patterns, otherwise you can configure them manually within Kibana settings. + + beats + , + "example": + ./packetbeat setup + , + "setup": + setup + , + } + } + />

- - auditbeat-* - - , - - filebeat-* - - , - - packetbeat-* - - , - - winlogbeat-* - +

} iconType="gisApp" title={

- Required Index Patterns Not Configured + Required index patterns not configured

} titleSize="xs" diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx new file mode 100644 index 0000000000000..49f5306dc1b60 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import React from 'react'; + +import '../../mock/ui_settings'; +import { TestProviders } from '../../mock'; +import { Embeddable } from './embeddable'; + +jest.mock('../../lib/settings/use_kibana_ui_setting'); + +describe('Embeddable', () => { + test('it renders', () => { + const wrapper = shallow( + + +

{'Test content'}

+
+
+ ); + + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.tsx new file mode 100644 index 0000000000000..b9a2d01ee0a70 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +const Panel = styled(EuiPanel)` + overflow: hidden; +`; +Panel.displayName = 'Panel'; + +export interface EmbeddableProps { + children: React.ReactNode; +} + +export const Embeddable = React.memo(({ children }) => ( +
+ {children} +
+)); +Embeddable.displayName = 'Embeddable'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx new file mode 100644 index 0000000000000..4536da3ba7b97 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import React from 'react'; + +import '../../mock/ui_settings'; +import { TestProviders } from '../../mock'; +import { EmbeddableHeader } from './embeddable_header'; + +jest.mock('../../lib/settings/use_kibana_ui_setting'); + +describe('EmbeddableHeader', () => { + test('it renders', () => { + const wrapper = shallow( + + + + ); + + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + test('it renders the title', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-embeddable-title"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders supplements when children provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect( + wrapper + .find('[data-test-subj="header-embeddable-supplements"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render supplements when children not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-embeddable-supplements"]') + .first() + .exists() + ).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx new file mode 100644 index 0000000000000..dbd9e3f763f92 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +const Header = styled.header.attrs({ + className: 'siemEmbeddable__header', +})` + ${({ theme }) => css` + border-bottom: ${theme.eui.euiBorderThin}; + padding: ${theme.eui.paddingSizes.m}; + `} +`; +Header.displayName = 'Header'; + +export interface EmbeddableHeaderProps { + children?: React.ReactNode; + title: string | React.ReactNode; +} + +export const EmbeddableHeader = React.memo(({ children, title }) => ( +
+ + + +
{title}
+
+
+ + {children && ( + + {children} + + )} +
+
+)); +EmbeddableHeader.displayName = 'EmbeddableHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index 87e38e0fac2e1..1c712f874969c 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -4,41 +4,74 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; import React, { useEffect, useState } from 'react'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import { Query } from 'src/plugins/data/common'; +import styled, { css } from 'styled-components'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; -import styled from 'styled-components'; -import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; import { EmbeddablePanel } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; - +import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; import { useIndexPatterns } from '../../hooks/use_index_patterns'; -import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { useKibanaPlugins } from '../../lib/compose/kibana_plugins'; import { useKibanaCore } from '../../lib/compose/kibana_core'; -import { useStateToaster } from '../toasters'; +import { useKibanaPlugins } from '../../lib/compose/kibana_plugins'; +import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; import { Loader } from '../loader'; +import { useStateToaster } from '../toasters'; +import { Embeddable } from './embeddable'; +import { EmbeddableHeader } from './embeddable_header'; +import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers'; import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; -import { MapEmbeddable, SetQuery } from './types'; +import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; +import { MapEmbeddable, SetQuery } from './types'; -import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers'; -import { MapToolTip } from './map_tool_tip/map_tool_tip'; +interface EmbeddableMapProps { + maintainRatio?: boolean; +} -const EmbeddableWrapper = styled(EuiFlexGroup)` - position: relative; - height: 400px; - margin: 0; +const EmbeddableMap = styled.div.attrs({ + className: 'siemEmbeddable__map', +})` + ${({ maintainRatio, theme }) => css` + .embPanel { + border: none; + box-shadow: none; + } + + .mapToolbarOverlay__button { + display: none; + } + + ${maintainRatio && + css` + padding-top: calc(3 / 4 * 100%); //4:3 (standard) ratio + position: relative; + + @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) { + padding-top: calc(9 / 32 * 100%); //32:9 (ultra widescreen) ratio + } - .mapToolbarOverlay__button { - display: none; - } + @media only screen and (min-width: 1441px) and (min-height: 901px) { + padding-top: calc(9 / 21 * 100%); //21:9 (ultrawide) ratio + } + + .embPanel { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + } + `} + `} `; +EmbeddableMap.displayName = 'EmbeddableMap'; export interface EmbeddedMapProps { query: Query; @@ -152,11 +185,23 @@ export const EmbeddedMap = React.memo( }, [startDate, endDate]); return isError ? null : ( - <> + + + + + {i18n.EMBEDDABLE_HEADER_HELP} + + + + - + + {embeddable != null ? ( ( ) : ( )} - - - + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx index 709725b853dd2..e71398455ee88 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx @@ -4,69 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { EuiButton, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; - +import { EuiButton, EuiCode, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import * as React from 'react'; import chrome from 'ui/chrome'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; import * as i18n from './translations'; -interface DocMapping { - beat: string; - docLink: string; -} - -export const IndexPatternsMissingPrompt = React.memo(() => { - const beatsSetupDocMapping: DocMapping[] = [ - { - beat: 'auditbeat', - docLink: `${ELASTIC_WEBSITE_URL}/guide/en/beats/auditbeat/${DOC_LINK_VERSION}/load-kibana-dashboards.html`, - }, - { - beat: 'filebeat', - docLink: `${ELASTIC_WEBSITE_URL}/guide/en/beats/filebeat/${DOC_LINK_VERSION}/load-kibana-dashboards.html`, - }, - { - beat: 'packetbeat', - docLink: `${ELASTIC_WEBSITE_URL}/guide/en/beats/packetbeat/${DOC_LINK_VERSION}/load-kibana-dashboards.html`, - }, - { - beat: 'winlogbeat', - docLink: `${ELASTIC_WEBSITE_URL}/guide/en/beats/winlogbeat/${DOC_LINK_VERSION}/load-kibana-dashboards.html`, - }, - ]; - - return ( - {i18n.ERROR_TITLE}} - titleSize="xs" - body={ - <> -

{i18n.ERROR_DESCRIPTION}

+export const IndexPatternsMissingPrompt = React.memo(() => ( + {i18n.ERROR_TITLE}} + titleSize="xs" + body={ + <> +

+ + {'beats'} + + ), + setup: {'setup'}, + example: {'./packetbeat setup'}, + }} + /> +

-

- {beatsSetupDocMapping - .map(v => ( - - {`${v.beat}-*`} - - )) - .reduce((acc, v) => [acc, ', ', v])} -

- - } - actions={ - - {i18n.ERROR_BUTTON} - - } - /> - ); -}); +

+ +

+ + } + actions={ + + {i18n.ERROR_BUTTON} + + } + /> +)); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts index 373bd1d054a43..958619bee19d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts @@ -6,6 +6,20 @@ import { i18n } from '@kbn/i18n'; +export const EMBEDDABLE_HEADER_TITLE = i18n.translate( + 'xpack.siem.components.embeddables.embeddedMap.embeddableHeaderTitle', + { + defaultMessage: 'Network map', + } +); + +export const EMBEDDABLE_HEADER_HELP = i18n.translate( + 'xpack.siem.components.embeddables.embeddedMap.embeddableHeaderHelp', + { + defaultMessage: 'Map configuration help', + } +); + export const MAP_TITLE = i18n.translate( 'xpack.siem.components.embeddables.embeddedMap.embeddablePanelTitle', { @@ -51,15 +65,7 @@ export const ERROR_CREATING_EMBEDDABLE = i18n.translate( export const ERROR_TITLE = i18n.translate( 'xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorTitle', { - defaultMessage: 'Required Index Patterns Not Configured', - } -); - -export const ERROR_DESCRIPTION = i18n.translate( - 'xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorDescription', - { - defaultMessage: - 'An ECS compliant Kibana index pattern must be configured to view event data on the map. When using beats, you can run the following setup commands to create the required Kibana index patterns, otherwise you can configure them manually within Kibana settings.', + defaultMessage: 'Required index patterns not configured', } ); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx index cb67736829878..c614fd52316bc 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { BrowserFields } from '../../containers/source'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; @@ -27,6 +27,7 @@ export const StatefulEventDetails = React.memo( ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { const [view, setView] = useState('table-view'); + const handleSetView = useCallback(newView => setView(newView), []); return ( ( data={data} id={id} onUpdateColumns={onUpdateColumns} - onViewSelected={newView => setView(newView)} + onViewSelected={handleSetView} timelineId={timelineId} toggleColumn={toggleColumn} view={view} diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 0ecfb15a67f3b..9483c60dcc552 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -6,7 +6,7 @@ import { Filter } from '@kbn/es-query'; import { isEqual } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { Query } from 'src/plugins/data/common'; @@ -102,32 +102,40 @@ const StatefulEventsViewerComponent = React.memo( }; }, []); - const onChangeItemsPerPage: OnChangeItemsPerPage = itemsChangedPerPage => - updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }); - - const toggleColumn = (column: ColumnHeader) => { - const exists = columns.findIndex(c => c.id === column.id) !== -1; + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), + [id, updateItemsPerPage] + ); - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } + const toggleColumn = useCallback( + (column: ColumnHeader) => { + const exists = columns.findIndex(c => c.id === column.id) !== -1; + + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id, + }); + } + }, + [columns, id, upsertColumn, removeColumn] + ); - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }; + const handleOnMouseEnter = useCallback(() => setShowInspect(true), []); + const handleOnMouseLeave = useCallback(() => setShowInspect(false), []); return ( {({ indexPattern, browserFields }) => ( -
setShowInspect(true)} onMouseLeave={() => setShowInspect(false)}> +
( width, }) => { /** Focuses the input that filters the field browser */ - function focusInput() { + const focusInput = () => { const elements = document.getElementsByClassName( getFieldBrowserSearchInputClassName(timelineId) ); @@ -130,22 +130,28 @@ export const FieldsBrowser = React.memo( if (elements.length > 0) { (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` } - } + }; /** Invoked when the user types in the input to filter the field browser */ - function onInputChange(event: React.ChangeEvent) { - onSearchInputChange(event.target.value); - } + const onInputChange = useCallback( + (event: React.ChangeEvent) => { + onSearchInputChange(event.target.value); + }, + [onSearchInputChange] + ); - function selectFieldAndHide(fieldId: string) { - if (onFieldSelected != null) { - onFieldSelected(fieldId); - } + const selectFieldAndHide = useCallback( + (fieldId: string) => { + if (onFieldSelected != null) { + onFieldSelected(fieldId); + } - onHideFieldBrowser(); - } + onHideFieldBrowser(); + }, + [onFieldSelected, onHideFieldBrowser] + ); - function scrollViews() { + const scrollViews = () => { if (selectedCategoryId !== '') { const categoryPaneTitles = document.getElementsByClassName( getCategoryPaneCategoryClassName({ @@ -171,7 +177,7 @@ export const FieldsBrowser = React.memo( } focusInput(); // always re-focus the input to enable additional filtering - } + }; useEffect(() => { scrollViews(); diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index 2a13d1549d90a..8948f765b8fbc 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -13,7 +13,7 @@ import { TestProviders } from '../../mock'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; -import { INPUT_TIMEOUT, StatefulFieldsBrowser } from '.'; +import { StatefulFieldsBrowser } from '.'; // Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 /* eslint-disable no-console */ const originalError = console.error; @@ -95,7 +95,7 @@ describe('StatefulFieldsBrowser', () => { describe('updateSelectedCategoryId', () => { beforeEach(() => { - jest.setTimeout(10000); + jest.useFakeTimers(); }); test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { const wrapper = mount( @@ -127,7 +127,8 @@ describe('StatefulFieldsBrowser', () => { wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first() ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); }); - test('it updates the selectedCategoryId state according to most fields returned', done => { + + test('it updates the selectedCategoryId state according to most fields returned', () => { const wrapper = mount( { .last() .simulate('change', { target: { value: 'cloud' } }); - setTimeout(() => { - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).first() - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - wrapper.unmount(); - done(); - }, INPUT_TIMEOUT); + jest.runOnlyPendingTimers(); + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).first() + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx index a58721eb5a87f..2c8092a3295ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx @@ -6,7 +6,7 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -82,77 +82,79 @@ export const StatefulFieldsBrowserComponent = React.memo { setShow(!show); - } + }, [show]); /** Invoked when the user types in the filter input */ - function updateFilter(newFilterInput: string) { - setFilterInput(newFilterInput); - setIsSearching(true); - - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: newFilterInput, - }); - - setFilteredBrowserFields(newFilteredBrowserFields); - setIsSearching(false); - - const newSelectedCategoryId = - newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(newFilteredBrowserFields) - .sort() - .reduce( - (selected, category) => - newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(newFilteredBrowserFields[category].fields!).length > - Object.keys(newFilteredBrowserFields[selected].fields!).length - ? category - : selected, - Object.keys(newFilteredBrowserFields)[0] - ); - setSelectedCategoryId(newSelectedCategoryId); - }, INPUT_TIMEOUT); - } + const updateFilter = useCallback( + (newFilterInput: string) => { + setFilterInput(newFilterInput); + setIsSearching(true); + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: newFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + + const newSelectedCategoryId = + newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(newFilteredBrowserFields) + .sort() + .reduce( + (selected, category) => + newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + Object.keys(newFilteredBrowserFields[category].fields!).length > + Object.keys(newFilteredBrowserFields[selected].fields!).length + ? category + : selected, + Object.keys(newFilteredBrowserFields)[0] + ); + setSelectedCategoryId(newSelectedCategoryId); + }, INPUT_TIMEOUT); + }, + [browserFields, filterInput, inputTimeoutId.current] + ); /** * Invoked when the user clicks a category name in the left-hand side of * the field browser */ - function updateSelectedCategoryId(categoryId: string) { + const updateSelectedCategoryId = useCallback((categoryId: string) => { setSelectedCategoryId(categoryId); - } + }, []); /** * Invoked when the user clicks on the context menu to view a category's * columns in the timeline, this function dispatches the action that * causes the timeline display those columns. */ - function updateColumnsAndSelectCategoryId(columns: ColumnHeader[]) { + const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeader[]) => { onUpdateColumns(columns); // show the category columns in the timeline - } + }, []); /** Invoked when the field browser should be hidden */ - function hideFieldBrowser() { + const hideFieldBrowser = useCallback(() => { setFilterInput(''); setFilterInput(''); setFilteredBrowserFields(null); setIsSearching(false); setSelectedCategoryId(DEFAULT_CATEGORY_NAME); setShow(false); - } + }, []); // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = show - ? mergeBrowserFieldsWithDefaultCategory(browserFields) - : {}; + const browserFieldsWithDefaultCategory = useMemo( + () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), + [show, browserFields] + ); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx index 1d0e03265f5ea..2f4da30672e8c 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { ActionCreator } from 'typescript-fsa'; @@ -22,7 +22,7 @@ import { import { UpdateNote } from '../../notes/helpers'; import { defaultHeaders } from '../../timeline/body/column_headers/default_headers'; import { Properties } from '../../timeline/properties'; -import { appActions } from '../../../store/app'; +import { appActions, appModel } from '../../../store/app'; import { inputsActions } from '../../../store/inputs'; import { timelineActions } from '../../../store/actions'; import { TimelineModel } from '../../../store/timeline/model'; @@ -36,7 +36,7 @@ interface OwnProps { interface StateReduxProps { description: string; - getNotesByIds: (noteIds: string[]) => Note[]; + notesById: appModel.NotesById; isDataInTimeline: boolean; isDatepickerLocked: boolean; isFavorite: boolean; @@ -75,13 +75,13 @@ const StatefulFlyoutHeader = React.memo( associateNote, createTimeline, description, - getNotesByIds, isFavorite, isDataInTimeline, isDatepickerLocked, title, width = DEFAULT_TIMELINE_WIDTH, noteIds, + notesById, timelineId, toggleLock, updateDescription, @@ -89,27 +89,33 @@ const StatefulFlyoutHeader = React.memo( updateNote, updateTitle, usersViewing, - }) => ( - - ) + }) => { + const getNotesByIds = useCallback( + (noteIdsVar: string[]): Note[] => appSelectors.getNotes(notesById, noteIdsVar), + [notesById] + ); + return ( + + ); + } ); StatefulFlyoutHeader.displayName = 'StatefulFlyoutHeader'; @@ -139,7 +145,7 @@ const makeMapStateToProps = () => { return { description, - getNotesByIds: getNotesByIds(state), + notesById: getNotesByIds(state), history, isDataInTimeline: !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index ceaff289f776c..ba5275ed79aef 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiToolTip } from '@elastic/eui'; -import * as React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -122,19 +122,22 @@ const FlyoutPaneComponent = React.memo( usersViewing, width, }) => { - const renderFlyout = () => <>; - - const onResize: OnResize = ({ delta, id }) => { - const bodyClientWidthPixels = document.body.clientWidth; - - applyDeltaToWidth({ - bodyClientWidthPixels, - delta, - id, - maxWidthPercent, - minWidthPixels, - }); - }; + const renderFlyout = useCallback(() => <>, []); + + const onResize: OnResize = useCallback( + ({ delta, id }) => { + const bodyClientWidthPixels = document.body.clientWidth; + + applyDeltaToWidth({ + bodyClientWidthPixels, + delta, + id, + maxWidthPercent, + minWidthPixels, + }); + }, + [applyDeltaToWidth, maxWidthPercent, minWidthPixels] + ); return ( > & { forceExpand?: boolean; @@ -42,19 +42,19 @@ export const LazyAccordion = React.memo( renderExpandedContent, }) => { const [expanded, setExpanded] = useState(false); - const onCollapsedClick = () => { + const onCollapsedClick = useCallback(() => { setExpanded(true); if (onExpand != null) { onExpand(); } - }; + }, [onExpand]); - const onExpandedClick = () => { + const onExpandedClick = useCallback(() => { setExpanded(false); if (onCollapse != null) { onCollapse(); } - }; + }, [onCollapse]); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx index aa9415aadeda1..6664660eb6bdc 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import styled from 'styled-components'; import { Note } from '../../../lib/note'; @@ -66,10 +66,13 @@ export const NoteCards = React.memo( }) => { const [newNote, setNewNote] = useState(''); - const associateNoteAndToggleShow = (noteId: string) => { - associateNote(noteId); - toggleShowAddNote(); - }; + const associateNoteAndToggleShow = useCallback( + (noteId: string) => { + associateNote(noteId); + toggleShowAddNote(); + }, + [associateNote, toggleShowAddNote] + ); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index d101d1f4d39f4..c96d25f0d11f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -5,7 +5,7 @@ */ import ApolloClient from 'apollo-client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; @@ -98,9 +98,9 @@ export const StatefulOpenTimelineComponent = React.memo( const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); /** Invoked when the user presses enters to submit the text in the search input */ - const onQueryChange: OnQueryChange = (query: EuiSearchBarQuery) => { + const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { setSearch(query.queryText.trim()); - }; + }, []); /** Focuses the input that filters the field browser */ const focusInput = () => { @@ -126,23 +126,26 @@ export const StatefulOpenTimelineComponent = React.memo( // } // }; - const onDeleteOneTimeline: OnDeleteOneTimeline = (timelineIds: string[]) => { - deleteTimelines(timelineIds, { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - }; + const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( + (timelineIds: string[]) => { + deleteTimelines(timelineIds, { + search, + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + sort: { + sortField: sortField as SortFieldTimeline, + sortOrder: sortDirection as Direction, + }, + onlyUserFavorite: onlyFavorites, + }); + }, + [search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites] + ); /** Invoked when the user clicks the action to delete the selected timelines */ - const onDeleteSelected: OnDeleteSelected = () => { + const onDeleteSelected: OnDeleteSelected = useCallback(() => { deleteTimelines(getSelectedTimelineIds(selectedItems), { search, pageInfo: { @@ -161,79 +164,81 @@ export const StatefulOpenTimelineComponent = React.memo( resetSelectionState(); // TODO: the query must re-execute to show the results of the deletion - }; + }, [selectedItems, search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites]); /** Invoked when the user selects (or de-selects) timelines */ - const onSelectionChange: OnSelectionChange = (newSelectedItems: OpenTimelineResult[]) => { - setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 - }; + const onSelectionChange: OnSelectionChange = useCallback( + (newSelectedItems: OpenTimelineResult[]) => { + setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 + }, + [] + ); /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ - const onTableChange: OnTableChange = ({ page, sort }: OnTableChangeParams) => { + const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { const { index, size } = page; const { field, direction } = sort; setPageIndex(index); setPageSize(size); setSortDirection(direction); setSortField(field); - }; + }, []); /** Invoked when the user toggles the option to only view favorite timelines */ - const onToggleOnlyFavorites: OnToggleOnlyFavorites = () => { + const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { setOnlyFavorites(!onlyFavorites); - }; + }, [onlyFavorites]); /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - const onToggleShowNotes: OnToggleShowNotes = ( - newItemIdToExpandedNotesRowMap: Record - ) => { - setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); - }; + const onToggleShowNotes: OnToggleShowNotes = useCallback( + (newItemIdToExpandedNotesRowMap: Record) => { + setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); + }, + [] + ); /** Resets the selection state such that all timelines are unselected */ - const resetSelectionState = () => { + const resetSelectionState = useCallback(() => { setSelectedItems([]); - }; + }, []); - const openTimeline: OnOpenTimeline = ({ - duplicate, - timelineId, - }: { - duplicate: boolean; - timelineId: string; - }) => { - if (isModal && closeModalTimeline != null) { - closeModalTimeline(); - } + const openTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + if (isModal && closeModalTimeline != null) { + closeModalTimeline(); + } - queryTimelineById({ - apolloClient, - duplicate, - timelineId, - updateIsLoading, - updateTimeline, - }); - }; + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + const deleteTimelines: DeleteTimelines = useCallback( + (timelineIds: string[], variables?: AllTimelinesVariables) => { + if (timelineIds.includes(timeline.savedObjectId || '')) { + createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + } + apolloClient.mutate({ + mutation: deleteTimelineMutation, + fetchPolicy: 'no-cache', + variables: { id: timelineIds }, + refetchQueries: [ + { + query: allTimelinesQuery, + variables, + }, + ], + }); + }, + [apolloClient, createNewTimeline, timeline] + ); - const deleteTimelines: DeleteTimelines = ( - timelineIds: string[], - variables?: AllTimelinesVariables - ) => { - if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); - } - apolloClient.mutate({ - mutation: deleteTimelineMutation, - fetchPolicy: 'no-cache', - variables: { id: timelineIds }, - refetchQueries: [ - { - query: allTimelinesQuery, - variables, - }, - ], - }); - }; useEffect(() => { focusInput(); }, []); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx index 41907e07d5c1b..e8242237cd2c8 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonEmpty, EuiModal, EuiOverlayMask } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { ApolloConsumer } from 'react-apollo'; import * as i18n from '../translations'; @@ -29,16 +29,20 @@ export const OpenTimelineModalButton = React.memo( const [showModal, setShowModal] = useState(false); /** shows or hides the `Open Timeline` modal */ - function toggleShowModal() { + const openModal = useCallback(() => { if (onToggle != null) { onToggle(); } - setShowModal(!showModal); - } + setShowModal(true); + }, [onToggle]); + + const closeModal = useCallback(() => { + if (onToggle != null) { + onToggle(); + } + setShowModal(false); + }, [onToggle]); - function closeModalTimeline() { - toggleShowModal(); - } return ( {client => ( @@ -48,7 +52,7 @@ export const OpenTimelineModalButton = React.memo( data-test-subj="open-timeline-button" iconSide="left" iconType="folderOpen" - onClick={toggleShowModal} + onClick={openModal} > {i18n.OPEN_TIMELINE} @@ -58,11 +62,11 @@ export const OpenTimelineModalButton = React.memo( ( updateTableActivePage, updateTableLimit, }) => { - const onChange = (criteria: Criteria) => { - if (criteria.sort != null) { - const sort: HostsSortField = { - field: getSortField(criteria.sort.field), - direction: criteria.sort.direction, - }; - if (sort.direction !== direction || sort.field !== sortField) { - updateHostsSort({ - sort, - hostsType: type, - }); + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const sort: HostsSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction, + }; + if (sort.direction !== direction || sort.field !== sortField) { + updateHostsSort({ + sort, + hostsType: type, + }); + } } - } - }; + }, + [direction, sortField, type] + ); const hostsColumns = useMemo(() => getHostsColumns(), []); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx index c1c1bec80d676..ac5470ee4f236 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -77,24 +77,34 @@ export const NetworkDnsTableComponent = React.memo( type, updateNetworkTable, }) => { - const onChange = (criteria: Criteria) => { - if (criteria.sort != null) { - const newDnsSortField: NetworkDnsSortField = { - field: criteria.sort.field.split('.')[1] as NetworkDnsFields, - direction: criteria.sort.direction, - }; - if (!isEqual(newDnsSortField, sort)) { - updateNetworkTable({ networkType: type, tableType, updates: { sort: newDnsSortField } }); + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newDnsSortField: NetworkDnsSortField = { + field: criteria.sort.field.split('.')[1] as NetworkDnsFields, + direction: criteria.sort.direction, + }; + if (!isEqual(newDnsSortField, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { sort: newDnsSortField }, + }); + } } - } - }; + }, + [sort, type] + ); - const onChangePtrIncluded = () => - updateNetworkTable({ - networkType: type, - tableType, - updates: { isPtrIncluded: !isPtrIncluded }, - }); + const onChangePtrIncluded = useCallback( + () => + updateNetworkTable({ + networkType: type, + tableType, + updates: { isPtrIncluded: !isPtrIncluded }, + }), + [type, isPtrIncluded] + ); return ( ( type, updateNetworkTable, }) => { - const onChange = (criteria: Criteria, tableType: networkModel.TopNTableType) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const field = last(splitField); - const newSortDirection = field !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopNFlowSort: NetworkTopTablesSortField = { - field: field as NetworkTopTablesFields, - direction: newSortDirection, - }; - if (!isEqual(newTopNFlowSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { - sort: newTopNFlowSort, - }, - }); + const onChange = useCallback( + (criteria: Criteria, tableType: networkModel.TopNTableType) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const field = last(splitField); + const newSortDirection = field !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click + const newTopNFlowSort: NetworkTopTablesSortField = { + field: field as NetworkTopTablesFields, + direction: newSortDirection, + }; + if (!isEqual(newTopNFlowSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { + sort: newTopNFlowSort, + }, + }); + } } - } - }; + }, + [sort, type] + ); let tableType: networkModel.TopNTableType; const headerTitle: string = diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx index 3a4712e53c4d4..6adb335839982 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { ActionCreator } from 'typescript-fsa'; @@ -78,22 +78,26 @@ const TlsTableComponent = React.memo( type === networkModel.NetworkType.page ? networkModel.NetworkTableType.tls : networkModel.IpDetailsTableType.tls; - const onChange = (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const newTlsSort: TlsSortField = { - field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction, - }; - if (!isEqual(newTlsSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { sort: newTlsSort }, - }); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const newTlsSort: TlsSortField = { + field: getSortFromString(splitField[splitField.length - 1]), + direction: criteria.sort.direction, + }; + if (!isEqual(newTlsSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { sort: newTlsSort }, + }); + } } - } - }; + }, + [sort, type] + ); return ( ( updateNetworkTable, sort, }) => { - const onChange = (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const newUsersSort: UsersSortField = { - field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction, - }; - if (!isEqual(newUsersSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { sort: newUsersSort }, - }); + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const newUsersSort: UsersSortField = { + field: getSortFromString(splitField[splitField.length - 1]), + direction: criteria.sort.direction, + }; + if (!isEqual(newUsersSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { sort: newUsersSort }, + }); + } } - } - }; + }, + [sort, type] + ); return ( { - const isQuickSelection = - payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now'); - let updateSearchBar: UpdateReduxSearchBar = { - id, - end: toStr != null ? toStr : new Date(end).toISOString(), - start: fromStr != null ? fromStr : new Date(start).toISOString(), - isInvalid: false, - isQuickSelection, - updateTime: false, - }; - let isStateUpdated = false; - - if ( - (isQuickSelection && - (fromStr !== payload.dateRange.from || toStr !== payload.dateRange.to)) || - (!isQuickSelection && - (start !== formatDate(payload.dateRange.from) || - end !== formatDate(payload.dateRange.to))) - ) { - isStateUpdated = true; - updateSearchBar.updateTime = true; - updateSearchBar.end = payload.dateRange.to; - updateSearchBar.start = payload.dateRange.from; - } - - if (payload.query != null && !isEqual(payload.query, filterQuery)) { - isStateUpdated = true; - updateSearchBar = set('query', payload.query, updateSearchBar); - } - - if (!isStateUpdated) { - // That mean we are doing a refresh! - if (isQuickSelection) { + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + const isQuickSelection = + payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now'); + let updateSearchBar: UpdateReduxSearchBar = { + id, + end: toStr != null ? toStr : new Date(end).toISOString(), + start: fromStr != null ? fromStr : new Date(start).toISOString(), + isInvalid: false, + isQuickSelection, + updateTime: false, + }; + let isStateUpdated = false; + + if ( + (isQuickSelection && + (fromStr !== payload.dateRange.from || toStr !== payload.dateRange.to)) || + (!isQuickSelection && + (start !== formatDate(payload.dateRange.from) || + end !== formatDate(payload.dateRange.to))) + ) { + isStateUpdated = true; updateSearchBar.updateTime = true; updateSearchBar.end = payload.dateRange.to; updateSearchBar.start = payload.dateRange.from; - } else { - queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); } - } - window.setTimeout(() => updateSearch(updateSearchBar), 0); - }; + if (payload.query != null && !isEqual(payload.query, filterQuery)) { + isStateUpdated = true; + updateSearchBar = set('query', payload.query, updateSearchBar); + } - const onRefresh = (payload: { dateRange: TimeRange }) => { - if (payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now')) { - updateSearch({ - id, - end: payload.dateRange.to, - start: payload.dateRange.from, - isInvalid: false, - isQuickSelection: true, - updateTime: true, - }); - } else { - queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - } - }; + if (!isStateUpdated) { + // That mean we are doing a refresh! + if (isQuickSelection) { + updateSearchBar.updateTime = true; + updateSearchBar.end = payload.dateRange.to; + updateSearchBar.start = payload.dateRange.from; + } else { + queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + } + } - const onSaved = (newSavedQuery: SavedQuery) => { - setSavedQuery({ id, savedQuery: newSavedQuery }); - }; + window.setTimeout(() => updateSearch(updateSearchBar), 0); + }, + [id, end, filterQuery, fromStr, queries, start, toStr] + ); - const onSavedQueryUpdated = (savedQueryUpdated: SavedQuery) => { - const isQuickSelection = savedQueryUpdated.attributes.timefilter - ? savedQueryUpdated.attributes.timefilter.from.includes('now') || - savedQueryUpdated.attributes.timefilter.to.includes('now') - : false; + const onRefresh = useCallback( + (payload: { dateRange: TimeRange }) => { + if (payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now')) { + updateSearch({ + id, + end: payload.dateRange.to, + start: payload.dateRange.from, + isInvalid: false, + isQuickSelection: true, + updateTime: true, + }); + } else { + queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + } + }, + [id, queries] + ); - let updateSearchBar: UpdateReduxSearchBar = { - id, - filters: savedQueryUpdated.attributes.filters || [], - end: toStr != null ? toStr : new Date(end).toISOString(), - start: fromStr != null ? fromStr : new Date(start).toISOString(), - isInvalid: false, - isQuickSelection, - updateTime: false, - }; + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + setSavedQuery({ id, savedQuery: newSavedQuery }); + }, + [id] + ); - if (savedQueryUpdated.attributes.timefilter) { - updateSearchBar.end = savedQueryUpdated.attributes.timefilter - ? savedQueryUpdated.attributes.timefilter.to - : updateSearchBar.end; - updateSearchBar.start = savedQueryUpdated.attributes.timefilter - ? savedQueryUpdated.attributes.timefilter.from - : updateSearchBar.start; - updateSearchBar.updateTime = true; - } + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const isQuickSelection = savedQueryUpdated.attributes.timefilter + ? savedQueryUpdated.attributes.timefilter.from.includes('now') || + savedQueryUpdated.attributes.timefilter.to.includes('now') + : false; + + let updateSearchBar: UpdateReduxSearchBar = { + id, + filters: savedQueryUpdated.attributes.filters || [], + end: toStr != null ? toStr : new Date(end).toISOString(), + start: fromStr != null ? fromStr : new Date(start).toISOString(), + isInvalid: false, + isQuickSelection, + updateTime: false, + }; + + if (savedQueryUpdated.attributes.timefilter) { + updateSearchBar.end = savedQueryUpdated.attributes.timefilter + ? savedQueryUpdated.attributes.timefilter.to + : updateSearchBar.end; + updateSearchBar.start = savedQueryUpdated.attributes.timefilter + ? savedQueryUpdated.attributes.timefilter.from + : updateSearchBar.start; + updateSearchBar.updateTime = true; + } - updateSearchBar = set('query', savedQueryUpdated.attributes.query, updateSearchBar); - updateSearchBar = set('savedQuery', savedQueryUpdated, updateSearchBar); + updateSearchBar = set('query', savedQueryUpdated.attributes.query, updateSearchBar); + updateSearchBar = set('savedQuery', savedQueryUpdated, updateSearchBar); - updateSearch(updateSearchBar); - }; + updateSearch(updateSearchBar); + }, + [id, end, fromStr, start, toStr] + ); - const onClearSavedQuery = () => { + const onClearSavedQuery = useCallback(() => { if (savedQuery != null) { updateSearch({ id, @@ -222,7 +234,7 @@ const SearchBarComponent = memo { let isSubscribed = true; @@ -246,13 +258,13 @@ const SearchBarComponent = memo [indexPattern as IndexPattern], [indexPattern]); return ( ( const [recentlyUsedRanges, setRecentlyUsedRanges] = useState( [] ); - const onRefresh = ({ start: newStart, end: newEnd }: OnRefreshProps): void => { - updateReduxTime({ - end: newEnd, - id, - isInvalid: false, - isQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const currentStart = formatDate(newStart); - const currentEnd = isQuickSelection - ? formatDate(newEnd, { roundUp: true }) - : formatDate(newEnd); - if (!isQuickSelection || (start === currentStart && end === currentEnd)) { - refetchQuery(queries); - } - }; - - const onRefreshChange = ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { - if (duration !== refreshInterval) { - setDuration({ id, duration: refreshInterval }); - } - - if (isPaused && policy === 'interval') { - stopAutoReload({ id }); - } else if (!isPaused && policy === 'manual') { - startAutoReload({ id }); - } - - if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { - refetchQuery(queries); - } - }; - - const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { - newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - const onTimeChange = ({ - start: newStart, - end: newEnd, - isQuickSelection: newIsQuickSelection, - isInvalid, - }: OnTimeChangeProps) => { - if (!isInvalid) { + const onRefresh = useCallback( + ({ start: newStart, end: newEnd }: OnRefreshProps): void => { updateReduxTime({ end: newEnd, id, - isInvalid, - isQuickSelection: newIsQuickSelection, + isInvalid: false, + isQuickSelection, kql: kqlQuery, start: newStart, timelineId, }); - const newRecentlyUsedRanges = [ - { start: newStart, end: newEnd }, - ...take( - MAX_RECENTLY_USED_RANGES, - recentlyUsedRanges.filter( - recentlyUsedRange => - !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) - ) - ), - ]; + const currentStart = formatDate(newStart); + const currentEnd = isQuickSelection + ? formatDate(newEnd, { roundUp: true }) + : formatDate(newEnd); + if (!isQuickSelection || (start === currentStart && end === currentEnd)) { + refetchQuery(queries); + } + }, + [end, id, isQuickSelection, kqlQuery, start, timelineId] + ); + + const onRefreshChange = useCallback( + ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { + if (duration !== refreshInterval) { + setDuration({ id, duration: refreshInterval }); + } - setRecentlyUsedRanges(newRecentlyUsedRanges); - setIsQuickSelection(newIsQuickSelection); - } + if (isPaused && policy === 'interval') { + stopAutoReload({ id }); + } else if (!isPaused && policy === 'manual') { + startAutoReload({ id }); + } + + if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { + refetchQuery(queries); + } + }, + [id, isQuickSelection, duration, policy, toStr] + ); + + const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { + newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); }; + + const onTimeChange = useCallback( + ({ + start: newStart, + end: newEnd, + isQuickSelection: newIsQuickSelection, + isInvalid, + }: OnTimeChangeProps) => { + if (!isInvalid) { + updateReduxTime({ + end: newEnd, + id, + isInvalid, + isQuickSelection: newIsQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const newRecentlyUsedRanges = [ + { start: newStart, end: newEnd }, + ...take( + MAX_RECENTLY_USED_RANGES, + recentlyUsedRanges.filter( + recentlyUsedRange => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + ), + ]; + + setRecentlyUsedRanges(newRecentlyUsedRanges); + setIsQuickSelection(newIsQuickSelection); + } + }, + [recentlyUsedRanges, kqlQuery] + ); + const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx index 7afa6ca70ff9b..96cb9f754f525 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx @@ -15,7 +15,7 @@ import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { ColumnHeader } from '../column_headers/column_header'; import { DataDrivenColumns } from '../data_driven_columns'; -import { eventHasNotes, eventIsPinned, getPinOnClick } from '../helpers'; +import { eventHasNotes, getPinOnClick } from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; interface Props { @@ -28,13 +28,13 @@ interface Props { eventIdToNoteIds: Readonly>; expanded: boolean; getNotesByIds: (noteIds: string[]) => Note[]; + isEventPinned: boolean; isEventViewer?: boolean; loading: boolean; onColumnResized: OnColumnResized; onEventToggled: () => void; onPinEvent: OnPinEvent; onUnPinEvent: OnUnPinEvent; - pinnedEventIds: Readonly>; showNotes: boolean; timelineId: string; toggleShowNotes: () => void; @@ -56,13 +56,13 @@ export const EventColumnView = React.memo( eventIdToNoteIds, expanded, getNotesByIds, + isEventPinned = false, isEventViewer = false, loading, onColumnResized, onEventToggled, onPinEvent, onUnPinEvent, - pinnedEventIds, showNotes, timelineId, toggleShowNotes, @@ -76,10 +76,7 @@ export const EventColumnView = React.memo( expanded={expanded} data-test-subj="actions" eventId={id} - eventIsPinned={eventIsPinned({ - eventId: id, - pinnedEventIds, - })} + eventIsPinned={isEventPinned} getNotesByIds={getNotesByIds} isEventViewer={isEventViewer} loading={loading} @@ -90,7 +87,7 @@ export const EventColumnView = React.memo( eventId: id, onPinEvent, onUnPinEvent, - pinnedEventIds, + isEventPinned, })} showCheckboxes={false} showNotes={showNotes} @@ -118,7 +115,7 @@ export const EventColumnView = React.memo( prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.expanded === nextProps.expanded && prevProps.loading === nextProps.loading && - prevProps.pinnedEventIds === nextProps.pinnedEventIds && + prevProps.isEventPinned === nextProps.isEventPinned && prevProps.showNotes === nextProps.showNotes && prevProps.timelineId === nextProps.timelineId ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx index 78992dceea4dc..34281219bcc0c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React from 'react'; import { BrowserFields } from '../../../../containers/source'; import { TimelineItem } from '../../../../graphql/types'; @@ -17,6 +17,7 @@ import { ColumnHeader } from '../column_headers/column_header'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { StatefulEvent } from './stateful_event'; +import { eventIsPinned } from '../helpers'; interface Props { actionsColumnWidth: number; @@ -74,6 +75,7 @@ export const Events = React.memo( event={event} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} + isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })} isEventViewer={isEventViewer} key={event._id} maxDelay={maxDelay(i)} @@ -81,7 +83,6 @@ export const Events = React.memo( onPinEvent={onPinEvent} onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} - pinnedEventIds={pinnedEventIds} rowRenderers={rowRenderers} timelineId={id} toggleColumn={toggleColumn} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index 766a75c05f17c..d54fe8df28a85 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; @@ -21,7 +21,6 @@ import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; import { useTimelineWidthContext } from '../../timeline_context'; import { ColumnHeader } from '../column_headers/column_header'; -import { eventIsPinned } from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getRowRenderer } from '../renderers/get_row_renderer'; import { RowRenderer } from '../renderers/row_renderer'; @@ -42,7 +41,7 @@ interface Props { onPinEvent: OnPinEvent; onUnPinEvent: OnUnPinEvent; onUpdateColumns: OnUpdateColumns; - pinnedEventIds: Readonly>; + isEventPinned: boolean; rowRenderers: RowRenderer[]; timelineId: string; toggleColumn: (column: ColumnHeader) => void; @@ -110,12 +109,12 @@ export const StatefulEvent = React.memo( eventIdToNoteIds, getNotesByIds, isEventViewer = false, + isEventPinned = false, maxDelay = 0, onColumnResized, onPinEvent, onUnPinEvent, onUpdateColumns, - pinnedEventIds, rowRenderers, timelineId, toggleColumn, @@ -127,27 +126,28 @@ export const StatefulEvent = React.memo( const divElement = useRef(null); - const onToggleShowNotes = (eventId: string): (() => void) => () => { + const onToggleShowNotes = useCallback(() => { + const eventId = event._id; setShowNotes({ ...showNotes, [eventId]: !showNotes[eventId] }); - }; + }, [event, showNotes]); - const onToggleExpanded = (eventId: string): (() => void) => () => { + const onToggleExpanded = useCallback(() => { + const eventId = event._id; setExpanded({ ...expanded, [eventId]: !expanded[eventId], }); - }; + }, [event, expanded]); - const associateNote = ( - eventId: string, - addNoteToEventChild: AddNoteToEvent, - onPinEventChild: OnPinEvent - ): ((noteId: string) => void) => (noteId: string) => { - addNoteToEventChild({ eventId, noteId }); - if (!eventIsPinned({ eventId, pinnedEventIds })) { - onPinEventChild(eventId); // pin the event, because it has notes - } - }; + const associateNote = useCallback( + (noteId: string) => { + addNoteToEvent({ eventId: event._id, noteId }); + if (!isEventPinned) { + onPinEvent(event._id); // pin the event, because it has notes + } + }, + [addNoteToEvent, event, isEventPinned, onPinEvent] + ); /** * Incrementally loads the events when it mounts by trying to @@ -155,7 +155,6 @@ export const StatefulEvent = React.memo( * indicate to React that it should render its self by setting * its initialRender to true. */ - useEffect(() => { let _isMounted = true; @@ -222,6 +221,7 @@ export const StatefulEvent = React.memo( expanded={!!expanded[event._id]} getNotesByIds={getNotesByIds} id={event._id} + isEventPinned={isEventPinned} isEventViewer={isEventViewer} loading={loading} onColumnResized={onColumnResized} @@ -229,7 +229,6 @@ export const StatefulEvent = React.memo( onToggleExpanded={onToggleExpanded} onToggleShowNotes={onToggleShowNotes} onUnPinEvent={onUnPinEvent} - pinnedEventIds={pinnedEventIds} showNotes={!!showNotes[event._id]} timelineId={timelineId} updateNote={updateNote} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx index 4a623ce99c9e9..668139349a377 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx @@ -29,21 +29,17 @@ interface Props { expanded: boolean; eventIdToNoteIds: Readonly>; isEventViewer?: boolean; + isEventPinned: boolean; loading: boolean; onColumnResized: OnColumnResized; onUnPinEvent: OnUnPinEvent; - pinnedEventIds: Readonly>; showNotes: boolean; timelineId: string; updateNote: UpdateNote; - onToggleExpanded: (eventId: string) => () => void; - onToggleShowNotes: (eventId: string) => () => void; + onToggleExpanded: () => void; + onToggleShowNotes: () => void; getNotesByIds: (noteIds: string[]) => Note[]; - associateNote: ( - eventId: string, - addNoteToEvent: AddNoteToEvent, - onPinEvent: OnPinEvent - ) => (noteId: string) => void; + associateNote: (noteId: string) => void; } export const getNewNoteId = (): string => uuid.v4(); @@ -64,11 +60,11 @@ export const StatefulEventChild = React.memo( eventIdToNoteIds, getNotesByIds, isEventViewer = false, + isEventPinned = false, loading, onColumnResized, onToggleExpanded, onUnPinEvent, - pinnedEventIds, showNotes, timelineId, onToggleShowNotes, @@ -84,23 +80,23 @@ export const StatefulEventChild = React.memo( @@ -110,13 +106,13 @@ export const StatefulEventChild = React.memo( style={{ width: `${width - OFFSET_SCROLLBAR}px` }} > diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts index 1cd83cb5560ea..0b1d21b2371ee 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts @@ -55,7 +55,7 @@ export interface GetPinOnClickParams { eventId: string; onPinEvent: OnPinEvent; onUnPinEvent: OnUnPinEvent; - pinnedEventIds: Readonly>; + isEventPinned: boolean; } export const getPinOnClick = ({ @@ -63,15 +63,12 @@ export const getPinOnClick = ({ eventId, onPinEvent, onUnPinEvent, - pinnedEventIds, + isEventPinned, }: GetPinOnClickParams): (() => void) => { if (!allowUnpinning) { return noop; } - - return eventIsPinned({ eventId, pinnedEventIds }) - ? () => onUnPinEvent(eventId) - : () => onPinEvent(eventId); + return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); }; export const getColumnWidthFromType = (type: string): number => diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx index 73b208757c7ad..86a6ebe22799b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx @@ -215,7 +215,7 @@ describe('Body', () => { wrapper.update(); wrapper .find('[data-test-subj="new-note-tabs"] textarea') - .simulate('change', { target: { value: 'hello world' } }); + .simulate('change', { target: { value: note } }); wrapper.update(); wrapper .find('button[data-test-subj="add-note"]') @@ -314,13 +314,11 @@ describe('Body', () => { /> ); addaNoteToEvent(wrapper, 'hello world'); - dispatchAddNoteToEvent.mockClear(); dispatchOnPinEvent.mockClear(); wrapper.setProps({ pinnedEventIds: { 1: true } }); wrapper.update(); addaNoteToEvent(wrapper, 'new hello world'); - expect(dispatchAddNoteToEvent).toHaveBeenCalled(); expect(dispatchOnPinEvent).not.toHaveBeenCalled(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index d93446b2af95b..531e61dd7dc60 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -6,14 +6,14 @@ import { noop } from 'lodash/fp'; import memoizeOne from 'memoize-one'; -import * as React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { BrowserFields } from '../../../containers/source'; import { TimelineItem } from '../../../graphql/types'; import { Note } from '../../../lib/note'; -import { appSelectors, State, timelineSelectors } from '../../../store'; +import { appModel, appSelectors, State, timelineSelectors } from '../../../store'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, @@ -45,7 +45,7 @@ interface OwnProps { interface ReduxProps { columnHeaders: ColumnHeader[]; eventIdToNoteIds: Readonly>; - getNotesByIds: (noteIds: string[]) => Note[]; + notesById: appModel.NotesById; pinnedEventIds: Readonly>; range?: string; } @@ -92,10 +92,10 @@ const StatefulBodyComponent = React.memo( columnHeaders, data, eventIdToNoteIds, - getNotesByIds, height, id, isEventViewer = false, + notesById, pinEvent, pinnedEventIds, range, @@ -107,30 +107,44 @@ const StatefulBodyComponent = React.memo( updateNote, updateSort, }) => { - const onAddNoteToEvent: AddNoteToEvent = ({ - eventId, - noteId, - }: { - eventId: string; - noteId: string; - }) => addNoteToEvent!({ id, eventId, noteId }); - - const onColumnSorted: OnColumnSorted = sorted => { - updateSort!({ id, sort: sorted }); - }; + const getNotesByIds = useCallback( + (noteIds: string[]): Note[] => appSelectors.getNotes(notesById, noteIds), + [notesById] + ); - const onColumnRemoved: OnColumnRemoved = columnId => removeColumn!({ id, columnId }); + const onAddNoteToEvent: AddNoteToEvent = useCallback( + ({ eventId, noteId }: { eventId: string; noteId: string }) => + addNoteToEvent!({ id, eventId, noteId }), + [id] + ); - const onColumnResized: OnColumnResized = ({ columnId, delta }) => - applyDeltaToColumnWidth!({ id, columnId, delta }); + const onColumnSorted: OnColumnSorted = useCallback( + sorted => { + updateSort!({ id, sort: sorted }); + }, + [id] + ); - const onPinEvent: OnPinEvent = eventId => pinEvent!({ id, eventId }); + const onColumnRemoved: OnColumnRemoved = useCallback( + columnId => removeColumn!({ id, columnId }), + [id] + ); - const onUnPinEvent: OnUnPinEvent = eventId => unPinEvent!({ id, eventId }); + const onColumnResized: OnColumnResized = useCallback( + ({ columnId, delta }) => applyDeltaToColumnWidth!({ id, columnId, delta }), + [id] + ); + + const onPinEvent: OnPinEvent = useCallback(eventId => pinEvent!({ id, eventId }), [id]); - const onUpdateNote: UpdateNote = (note: Note) => updateNote!({ note }); + const onUnPinEvent: OnUnPinEvent = useCallback(eventId => unPinEvent!({ id, eventId }), [id]); - const onUpdateColumns: OnUpdateColumns = columns => updateColumns!({ id, columns }); + const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), []); + + const onUpdateColumns: OnUpdateColumns = useCallback( + columns => updateColumns!({ id, columns }), + [id] + ); return ( ( prevProps.columnHeaders === nextProps.columnHeaders && prevProps.data === nextProps.data && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.getNotesByIds === nextProps.getNotesByIds && + prevProps.notesById === nextProps.notesById && prevProps.height === nextProps.height && prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && @@ -194,7 +208,7 @@ const makeMapStateToProps = () => { return { columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, - getNotesByIds: getNotesByIds(state), + notesById: getNotesByIds(state), id, pinnedEventIds, }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx index 98cf0a78b1d1f..79f9c32a176f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx @@ -5,7 +5,7 @@ */ import { noop } from 'lodash/fp'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { BrowserFields } from '../../../containers/source'; @@ -51,23 +51,23 @@ export const ProviderItemBadge = React.memo( }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - function togglePopover() { + const togglePopover = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); - } + }, [isPopoverOpen]); - function closePopover() { + const closePopover = useCallback(() => { setIsPopoverOpen(false); - } + }, []); - function onToggleEnabledProvider() { + const onToggleEnabledProvider = useCallback(() => { toggleEnabledProvider(); closePopover(); - } + }, [toggleEnabledProvider]); - function onToggleExcludedProvider() { + const onToggleExcludedProvider = useCallback(() => { toggleExcludedProvider(); closePopover(); - } + }, [toggleExcludedProvider]); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index c1772d9e55577..a0942cbaba091 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -18,7 +18,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; @@ -182,14 +182,14 @@ export const Footer = React.memo( const [paginationLoading, setPaginationLoading] = useState(false); const [updatedAt, setUpdatedAt] = useState(null); - const loadMore = () => { + const loadMore = useCallback(() => { setPaginationLoading(true); onLoadMore(nextCursor, tieBreaker); - }; + }, [nextCursor, tieBreaker, onLoadMore]); - const onButtonClick = () => setIsPopoverOpen(!isPopoverOpen); + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - const closePopover = () => setIsPopoverOpen(false); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); useEffect(() => { if (paginationLoading && !isLoading) { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index ab92f22a4c89f..78a9488b2fdbb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -159,79 +159,84 @@ const StatefulTimelineComponent = React.memo( updateItemsPerPage, upsertColumn, }) => { - const onDataProviderRemoved: OnDataProviderRemoved = ( - providerId: string, - andProviderId?: string - ) => removeProvider!({ id, providerId, andProviderId }); + const onDataProviderRemoved: OnDataProviderRemoved = useCallback( + (providerId: string, andProviderId?: string) => + removeProvider!({ id, providerId, andProviderId }), + [id] + ); - const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = ({ - providerId, - enabled, - andProviderId, - }) => - updateDataProviderEnabled!({ - id, - enabled, - providerId, - andProviderId, - }); + const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = useCallback( + ({ providerId, enabled, andProviderId }) => + updateDataProviderEnabled!({ + id, + enabled, + providerId, + andProviderId, + }), + [id] + ); - const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = ({ - providerId, - excluded, - andProviderId, - }) => - updateDataProviderExcluded!({ - id, - excluded, - providerId, - andProviderId, - }); + const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = useCallback( + ({ providerId, excluded, andProviderId }) => + updateDataProviderExcluded!({ + id, + excluded, + providerId, + andProviderId, + }), + [id] + ); - const onDataProviderEditedLocal: OnDataProviderEdited = ({ - andProviderId, - excluded, - field, - operator, - providerId, - value, - }) => - onDataProviderEdited!({ - andProviderId, - excluded, - field, - id, - operator, - providerId, - value, - }); - const onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery = ({ providerId, kqlQuery }) => - updateDataProviderKqlQuery!({ id, kqlQuery, providerId }); + const onDataProviderEditedLocal: OnDataProviderEdited = useCallback( + ({ andProviderId, excluded, field, operator, providerId, value }) => + onDataProviderEdited!({ + andProviderId, + excluded, + field, + id, + operator, + providerId, + value, + }), + [id] + ); - const onChangeItemsPerPage: OnChangeItemsPerPage = itemsChangedPerPage => - updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }); + const onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery = useCallback( + ({ providerId, kqlQuery }) => updateDataProviderKqlQuery!({ id, kqlQuery, providerId }), + [id] + ); - const onChangeDroppableAndProvider: OnChangeDroppableAndProvider = providerId => - updateHighlightedDropAndProviderId!({ id, providerId }); + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + itemsChangedPerPage => updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }), + [id] + ); - const toggleColumn = (column: ColumnHeader) => { - const exists = columns.findIndex(c => c.id === column.id) !== -1; + const onChangeDroppableAndProvider: OnChangeDroppableAndProvider = useCallback( + providerId => updateHighlightedDropAndProviderId!({ id, providerId }), + [id] + ); - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } + const toggleColumn = useCallback( + (column: ColumnHeader) => { + const exists = columns.findIndex(c => c.id === column.id) !== -1; - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }; + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id, + }); + } + }, + [columns, id] + ); useEffect(() => { if (createTimeline != null) { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index b983963c34f55..111e31479932a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -5,7 +5,7 @@ */ import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { Note } from '../../../lib/note'; @@ -114,17 +114,17 @@ export const Properties = React.memo( const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); - const onButtonClick = () => { + const onButtonClick = useCallback(() => { setShowActions(!showActions); - }; + }, [showActions]); - const onToggleShowNotes = () => { + const onToggleShowNotes = useCallback(() => { setShowNotes(!showNotes); - }; + }, [showNotes]); - const onClosePopover = () => { + const onClosePopover = useCallback(() => { setShowActions(false); - }; + }, []); const datePickerWidth = width - diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index 91113a545821d..0ebceccfa90c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -5,7 +5,7 @@ */ import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { StaticIndexPattern } from 'ui/index_patterns'; @@ -56,26 +56,32 @@ const StatefulSearchOrFilterComponent = React.memo( timelineId, updateKqlMode, }) => { - const applyFilterQueryFromKueryExpression = (expression: string) => - applyKqlFilterQuery({ - id: timelineId, - filterQuery: { - kuery: { + const applyFilterQueryFromKueryExpression = useCallback( + (expression: string) => + applyKqlFilterQuery({ + id: timelineId, + filterQuery: { + kuery: { + kind: 'kuery', + expression, + }, + serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), + }, + }), + [indexPattern, timelineId] + ); + + const setFilterQueryDraftFromKueryExpression = useCallback( + (expression: string) => + setKqlFilterQueryDraft({ + id: timelineId, + filterQueryDraft: { kind: 'kuery', expression, }, - serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), - }, - }); - - const setFilterQueryDraftFromKueryExpression = (expression: string) => - setKqlFilterQueryDraft({ - id: timelineId, - filterQueryDraft: { - kind: 'kuery', - expression, - }, - }); + }), + [timelineId] + ); return ( ( ({ alwaysShow = false, hoverContent, render }) => { const [showHoverContent, setShowHoverContent] = useState(false); - function onMouseEnter() { + const onMouseEnter = useCallback(() => { setShowHoverContent(true); - } + }, []); - function onMouseLeave() { + const onMouseLeave = useCallback(() => { setShowHoverContent(false); - } + }, []); + return ( <>{render(showHoverContent)} diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index edd5740f62879..ff6e5e4d0c788 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -57,30 +57,28 @@ interface WithSourceProps { sourceId: string; } -export const WithSource = React.memo(({ children, sourceId }) => { - const getIndexFields = (title: string, fields: IndexField[]): StaticIndexPattern => +const getIndexFields = memoizeOne( + (title: string, fields: IndexField[]): StaticIndexPattern => fields && fields.length > 0 ? { fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), title, } - : { fields: [], title }; + : { fields: [], title } +); - const getBrowserFields = (fields: IndexField[]): BrowserFields => +const getBrowserFields = memoizeOne( + (fields: IndexField[]): BrowserFields => fields && fields.length > 0 ? fields.reduce( (accumulator: BrowserFields, field: IndexField) => set([field.category, 'fields', field.name], field, accumulator), {} ) - : {}; - const getBrowserFieldsMemo: (fields: IndexField[]) => BrowserFields = memoizeOne( - getBrowserFields - ); - const getIndexFieldsMemo: ( - title: string, - fields: IndexField[] - ) => StaticIndexPattern = memoizeOne(getIndexFields); + : {} +); + +export const WithSource = React.memo(({ children, sourceId }) => { return ( query={sourceQuery} @@ -94,8 +92,8 @@ export const WithSource = React.memo(({ children, sourceId }) = {({ data }) => children({ indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFieldsMemo(get('source.status.indexFields', data)), - indexPattern: getIndexFieldsMemo( + browserFields: getBrowserFields(get('source.status.indexFields', data)), + indexPattern: getIndexFields( chrome .getUiSettingsClient() .get(DEFAULT_INDEX_KEY) diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx index c3bff998fdefd..5ff28480f1b3f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx @@ -36,43 +36,43 @@ interface OwnProps extends AllTimelinesVariables { children?: (args: AllTimelinesArgs) => React.ReactNode; } -const getAllTimeline = (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => - timelines.map(timeline => ({ - created: timeline.created, - description: timeline.description, - eventIdToNoteIds: - timeline.eventIdToNoteIds != null - ? timeline.eventIdToNoteIds.reduce((acc, note) => { - if (note.eventId != null) { - const notes = getOr([], note.eventId, acc); - return { ...acc, [note.eventId]: [...notes, note.noteId] }; - } - return acc; - }, {}) - : null, - favorite: timeline.favorite, - noteIds: timeline.noteIds, - notes: - timeline.notes != null - ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) - : null, - pinnedEventIds: - timeline.pinnedEventIds != null - ? timeline.pinnedEventIds.reduce( - (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), - {} - ) - : null, - savedObjectId: timeline.savedObjectId, - title: timeline.title, - updated: timeline.updated, - updatedBy: timeline.updatedBy, - })); +const getAllTimeline = memoizeOne( + (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => + timelines.map(timeline => ({ + created: timeline.created, + description: timeline.description, + eventIdToNoteIds: + timeline.eventIdToNoteIds != null + ? timeline.eventIdToNoteIds.reduce((acc, note) => { + if (note.eventId != null) { + const notes = getOr([], note.eventId, acc); + return { ...acc, [note.eventId]: [...notes, note.noteId] }; + } + return acc; + }, {}) + : null, + favorite: timeline.favorite, + noteIds: timeline.noteIds, + notes: + timeline.notes != null + ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) + : null, + pinnedEventIds: + timeline.pinnedEventIds != null + ? timeline.pinnedEventIds.reduce( + (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), + {} + ) + : null, + savedObjectId: timeline.savedObjectId, + title: timeline.title, + updated: timeline.updated, + updatedBy: timeline.updatedBy, + })) +); export const AllTimelinesQuery = React.memo( ({ children, onlyUserFavorite, pageInfo, search, sort }) => { - const memoizedAllTimeline = memoizeOne(getAllTimeline); - const variables: GetAllTimeline.Variables = { onlyUserFavorite, pageInfo, @@ -90,7 +90,7 @@ export const AllTimelinesQuery = React.memo( return children!({ loading, totalCount: getOr(0, 'getAllTimeline.totalCount', data), - timelines: memoizedAllTimeline( + timelines: getAllTimeline( JSON.stringify(variables), getOr([], 'getAllTimeline.timeline', data) ), diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx index 54dd44063f5da..cfb3f8bd8dc77 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx @@ -28,11 +28,12 @@ export interface TimelineDetailsProps { sourceId: string; } +const getDetailsEvent = memoizeOne( + (variables: string, detail: DetailItem[]): DetailItem[] => detail +); + export const TimelineDetailsComponentQuery = React.memo( ({ children, indexName, eventId, executeQuery, sourceId }) => { - const getDetailsEvent = (variables: string, detail: DetailItem[]): DetailItem[] => detail; - const getDetailsEventMemo = memoizeOne(getDetailsEvent); - const variables: GetTimelineDetailsQuery.Variables = { sourceId, indexName, @@ -49,7 +50,7 @@ export const TimelineDetailsComponentQuery = React.memo( {({ data, loading, refetch }) => { return children!({ loading, - detailsData: getDetailsEventMemo( + detailsData: getDetailsEvent( JSON.stringify(variables), getOr([], 'source.TimelineDetails.data', data) ), diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/__snapshots__/body.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/hosts/details/__snapshots__/body.test.tsx.snap deleted file mode 100644 index 3815b319820ef..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/__snapshots__/body.test.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`body render snapshot 1`] = ` - - - - - -`; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.test.tsx deleted file mode 100644 index 83af0a616a660..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.test.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow, mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import React from 'react'; -import { StaticIndexPattern } from 'ui/index_patterns'; - -import { mockIndexPattern } from '../../../mock/index_pattern'; -import { TestProviders } from '../../../mock/test_providers'; -import { mockUiSettings } from '../../../mock/ui_settings'; -import { CommonChildren } from '../navigation/types'; -import { HostDetailsBody } from './body'; -import { useKibanaCore } from '../../../lib/compose/kibana_core'; - -const mockUseKibanaCore = useKibanaCore as jest.Mock; -jest.mock('../../../lib/compose/kibana_core'); -mockUseKibanaCore.mockImplementation(() => ({ - uiSettings: mockUiSettings, -})); - -jest.mock('../../../containers/source', () => ({ - indicesExistOrDataTemporarilyUnavailable: () => true, - WithSource: ({ - children, - }: { - children: (args: { - indicesExist: boolean; - indexPattern: StaticIndexPattern; - }) => React.ReactNode; - }) => children({ indicesExist: true, indexPattern: mockIndexPattern }), -})); - -describe('body', () => { - test('render snapshot', () => { - const child: CommonChildren = () => {'I am a child'}; - const wrapper = shallow( - - {}} - to={0} - /> - - ); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - test('it should pass expected object properties to children', () => { - const child = jest.fn(); - mount( - - {}} - to={0} - /> - - ); - // match against everything but the functions to ensure they are there as expected - expect(child.mock.calls[0][0]).toMatchObject({ - endDate: 0, - filterQuery: - '{"bool":{"must":[],"filter":[{"match_all":{}},{"match_phrase":{"host.name":{"query":"host-1"}}}],"should":[],"must_not":[]}}', - skip: false, - startDate: 0, - type: 'details', - indexPattern: { - fields: [ - { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, - { name: '@version', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.id', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test1', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test2', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test3', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test4', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test5', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test6', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test7', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.test8', searchable: true, type: 'string', aggregatable: true }, - { name: 'host.name', searchable: true, type: 'string', aggregatable: true }, - ], - title: 'filebeat-*,auditbeat-*,packetbeat-*', - }, - hostName: 'host-1', - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx deleted file mode 100644 index ae8ebcf41cd56..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getEsQueryConfig } from '@kbn/es-query'; -import React from 'react'; -import { connect } from 'react-redux'; - -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { Anomaly } from '../../../components/ml/types'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { useKibanaCore } from '../../../lib/compose/kibana_core'; - -import { HostDetailsBodyComponentProps } from './types'; -import { type, makeMapStateToProps } from './utils'; - -const HostDetailsBodyComponent = React.memo( - ({ - children, - deleteQuery, - detailName, - filters, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setQuery, - to, - }) => { - const core = useKibanaCore(); - return ( - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: getEsQueryConfig(core.uiSettings), - indexPattern, - queries: [query], - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: detailName, - params: { - query: detailName, - }, - }, - query: { - match: { - 'host.name': { - query: detailName, - type: 'phrase', - }, - }, - }, - }, - ...filters, - ], - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - <> - {children({ - deleteQuery, - endDate: to, - filterQuery, - skip: isInitializing, - setQuery, - startDate: from, - type, - indexPattern, - hostName: detailName, - narrowDateRange: (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - updateDateRange: (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - })} - - ) : null; - }} - - ); - } -); - -HostDetailsBodyComponent.displayName = 'HostDetailsBodyComponent'; - -export const HostDetailsBody = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - } -)(HostDetailsBodyComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx new file mode 100644 index 0000000000000..6ceebc1708b18 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { MemoryRouter } from 'react-router-dom'; + +import { mockIndexPattern } from '../../../mock/index_pattern'; +import { TestProviders } from '../../../mock/test_providers'; +import { mockUiSettings } from '../../../mock/ui_settings'; +import { HostDetailsTabs } from './details_tabs'; +import { SetAbsoluteRangeDatePicker } from './types'; +import { hostDetailsPagePath } from '../types'; +import { type } from './utils'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; + +jest.mock('../../../lib/settings/use_kibana_ui_setting'); + +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, +})); + +jest.mock('../../../containers/source', () => ({ + indicesExistOrDataTemporarilyUnavailable: () => true, + WithSource: ({ + children, + }: { + children: (args: { + indicesExist: boolean; + indexPattern: StaticIndexPattern; + }) => React.ReactNode; + }) => children({ indicesExist: true, indexPattern: mockIndexPattern }), +})); + +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar +jest.mock('../../../components/search_bar', () => ({ + SiemSearchBar: () => null, +})); + +describe('body', () => { + const scenariosMap = { + authentications: 'AuthenticationsQueryTabBody', + allHosts: 'HostsQueryTabBody', + uncommonProcesses: 'UncommonProcessQueryTabBody', + anomalies: 'AnomaliesQueryTabBody', + events: 'EventsQueryTabBody', + }; + + Object.entries(scenariosMap).forEach(([path, componentName]) => + test(`it should pass expected object properties to ${componentName}`, () => { + const wrapper = mount( + + + {}} + to={0} + setAbsoluteRangeDatePicker={(jest.fn() as unknown) as SetAbsoluteRangeDatePicker} + hostDetailsPagePath={hostDetailsPagePath} + indexPattern={mockIndexPattern} + type={type} + filterQuery='{"bool":{"must":[],"filter":[{"match_all":{}},{"match_phrase":{"host.name":{"query":"host-1"}}}],"should":[],"must_not":[]}}' + /> + + + ); + + // match against everything but the functions to ensure they are there as expected + expect(wrapper.find(componentName).props()).toMatchObject({ + endDate: 0, + filterQuery: + '{"bool":{"must":[],"filter":[{"match_all":{}},{"match_phrase":{"host.name":{"query":"host-1"}}}],"should":[],"must_not":[]}}', + skip: false, + startDate: 0, + type: 'details', + indexPattern: { + fields: [ + { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, + { name: '@version', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.id', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test1', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test2', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test3', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test4', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test5', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test6', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test7', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.test8', searchable: true, type: 'string', aggregatable: true }, + { name: 'host.name', searchable: true, type: 'string', aggregatable: true }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', + }, + hostName: 'host-1', + }); + }) + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx new file mode 100644 index 0000000000000..48b6d34d0b28b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../../components/ml/types'; +import { HostsTableType } from '../../../store/hosts/model'; + +import { HostDetailsTabsProps } from './types'; +import { type } from './utils'; + +import { + HostsQueryTabBody, + AuthenticationsQueryTabBody, + UncommonProcessQueryTabBody, + AnomaliesQueryTabBody, + EventsQueryTabBody, +} from '../navigation'; + +const HostDetailsTabs = React.memo( + ({ + deleteQuery, + filterQuery, + from, + isInitializing, + detailName, + setAbsoluteRangeDatePicker, + setQuery, + to, + indexPattern, + hostDetailsPagePath, + }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker, scoreIntervalToDateTime] + ); + + const updateDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker, scoreIntervalToDateTime] + ); + + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + skip: isInitializing, + setQuery, + startDate: from, + type, + indexPattern, + hostName: detailName, + narrowDateRange, + updateDateRange, + }; + + return ( + + } + /> + } + /> + } + /> + } + /> + } + /> + + ); + } +); + +HostDetailsTabs.displayName = 'HostDetailsTabs'; + +export { HostDetailsTabs }; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx index d1d29c3d2ea82..aa81481bedbe7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx @@ -11,6 +11,8 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; +import { inputsSelectors, State } from '../../../store'; + import { FiltersGlobal } from '../../../components/filters_global'; import { HeaderPage } from '../../../components/header_page'; import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; @@ -32,22 +34,19 @@ import { LastEventIndexKey } from '../../../graphql/types'; import { convertToBuildEsQuery } from '../../../lib/keury'; import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; import { SpyRoute } from '../../../utils/route/spy_routes'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; -import { HostsQueryProps } from '../hosts'; import { HostsEmptyPage } from '../hosts_empty_page'; - -export { HostDetailsBody } from './body'; import { navTabsHostDetails } from './nav_tabs'; -import { HostDetailsComponentProps } from './types'; -import { makeMapStateToProps } from './utils'; -import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { HostDetailsComponentProps, HostDetailsProps } from './types'; +import { HostDetailsTabs } from './details_tabs'; +import { type } from './utils'; const HostOverviewManage = manageQuery(HostOverview); const KpiHostDetailsManage = manageQuery(KpiHostsComponent); const HostDetailsComponent = React.memo( ({ - detailName, filters, from, isInitializing, @@ -56,6 +55,9 @@ const HostDetailsComponent = React.memo( setHostDetailsTablesActivePageToZero, setQuery, to, + detailName, + deleteQuery, + hostDetailsPagePath, }) => { useEffect(() => { setHostDetailsTablesActivePageToZero(null); @@ -96,94 +98,111 @@ const HostDetailsComponent = React.memo( ], }); return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + <> + + + + - - } - title={detailName} - /> - - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - + + } + title={detailName} + /> + + {({ hostOverview, loading, id, inspect, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + - + - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }} - /> - )} - + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }} + /> + )} + - + + + - - + ) : ( <> - ); @@ -197,7 +216,16 @@ const HostDetailsComponent = React.memo( HostDetailsComponent.displayName = 'HostDetailsComponent'; -export const HostDetails = compose>( +export const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +export const HostDetails = compose>( connect( makeMapStateToProps, { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts index b4dda2cee8760..9df57970176eb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { StaticIndexPattern } from 'ui/index_patterns'; import { Filter } from '@kbn/es-query'; import { ActionCreator } from 'typescript-fsa'; import { Query } from 'src/plugins/data/common'; @@ -11,13 +12,10 @@ import { Query } from 'src/plugins/data/common'; import { InputsModelId } from '../../../store/inputs/constants'; import { HostComponentProps } from '../../../components/link_to/redirect_to_hosts'; import { HostsTableType } from '../../../store/hosts/model'; -import { HostsQueryProps } from '../hosts'; +import { HostsQueryProps } from '../types'; import { NavTab } from '../../../components/navigation/types'; -import { - AnomaliesChildren, - CommonChildren, - KeyHostsNavTabWithoutMlPermission, -} from '../navigation/types'; +import { KeyHostsNavTabWithoutMlPermission } from '../navigation/types'; +import { hostsModel } from '../../../store'; interface HostDetailsComponentReduxProps { query: Query; @@ -31,14 +29,16 @@ interface HostBodyComponentDispatchProps { to: number; }>; detailName: string; + hostDetailsPagePath: string; } interface HostDetailsComponentDispatchProps extends HostBodyComponentDispatchProps { setHostDetailsTablesActivePageToZero: ActionCreator; } -export interface HostDetailsBodyProps extends HostsQueryProps { - children: CommonChildren | AnomaliesChildren; +export interface HostDetailsProps extends HostsQueryProps { + detailName: string; + hostDetailsPagePath: string; } export type HostDetailsComponentProps = HostDetailsComponentReduxProps & @@ -46,10 +46,6 @@ export type HostDetailsComponentProps = HostDetailsComponentReduxProps & HostComponentProps & HostsQueryProps; -export type HostDetailsBodyComponentProps = HostDetailsComponentReduxProps & - HostBodyComponentDispatchProps & - HostDetailsBodyProps; - type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications & HostsTableType.uncommonProcesses & HostsTableType.events; @@ -62,3 +58,16 @@ type KeyHostDetailsNavTab = | KeyHostDetailsNavTabWithMlPermission; export type HostDetailsNavTab = Record; + +export type HostDetailsTabsProps = HostBodyComponentDispatchProps & + HostsQueryProps & { + indexPattern: StaticIndexPattern; + type: hostsModel.HostsType; + filterQuery: string; + }; + +export type SetAbsoluteRangeDatePicker = ActionCreator<{ + id: InputsModelId; + from: number; + to: number; +}>; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts index 52efe93c0c8dc..7483636cfe03d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts @@ -6,7 +6,7 @@ import { Breadcrumb } from 'ui/chrome'; -import { hostsModel, inputsSelectors, State } from '../../../store'; +import { hostsModel } from '../../../store'; import { HostsTableType } from '../../../store/hosts/model'; import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; @@ -15,15 +15,6 @@ import { RouteSpyState } from '../../../utils/route/types'; export const type = hostsModel.HostsType.details; -export const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - const TabNameMappedToI18nKey = { [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index 5b6444148045d..d2c9822889c26 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -19,7 +19,8 @@ import { wait } from '../../lib/helpers'; import { TestProviders } from '../../mock'; import { mockUiSettings } from '../../mock/ui_settings'; import { InputsModelId } from '../../store/inputs/constants'; -import { Hosts, HostsComponentProps } from './hosts'; +import { HostsComponentProps } from './types'; +import { Hosts } from './hosts'; import { useKibanaCore } from '../../lib/compose/kibana_core'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -97,6 +98,7 @@ describe('Hosts - rendering', () => { }>, query: { query: '', language: 'kuery' }, filters: [], + hostsPagePath: '', }; beforeAll(() => { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 7c54745f872a9..334d730378b23 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -5,13 +5,11 @@ */ import { EuiSpacer } from '@elastic/eui'; -import { Filter, getEsQueryConfig } from '@kbn/es-query'; +import { getEsQueryConfig } from '@kbn/es-query'; import * as React from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; -import { ActionCreator } from 'typescript-fsa'; -import { Query } from 'src/plugins/data/common'; import { FiltersGlobal } from '../../components/filters_global'; import { GlobalTimeArgs } from '../../containers/global_time'; @@ -27,41 +25,34 @@ import { SiemSearchBar } from '../../components/search_bar'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { convertToBuildEsQuery } from '../../lib/keury'; -import { inputsSelectors, State } from '../../store'; +import { inputsSelectors, State, hostsModel } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { InputsModelId } from '../../store/inputs/constants'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { useKibanaCore } from '../../lib/compose/kibana_core'; import { HostsEmptyPage } from './hosts_empty_page'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; -import { useKibanaCore } from '../../lib/compose/kibana_core'; +import { HostsComponentProps, HostsComponentReduxProps } from './types'; +import { HostsTabs } from './hosts_tabs'; const KpiHostsComponentManage = manageQuery(KpiHostsComponent); -interface HostsComponentReduxProps { - query: Query; - filters: Filter[]; -} - -interface HostsComponentDispatchProps { - setAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; -} - -export type HostsQueryProps = GlobalTimeArgs; - -export type HostsComponentProps = HostsComponentReduxProps & - HostsComponentDispatchProps & - HostsQueryProps; - const HostsComponent = React.memo( - ({ isInitializing, filters, from, query, setAbsoluteRangeDatePicker, setQuery, to }) => { + ({ + deleteQuery, + isInitializing, + filters, + from, + query, + setAbsoluteRangeDatePicker, + setQuery, + to, + hostsPagePath, + }) => { const capabilities = React.useContext(MlCapabilitiesContext); const core = useKibanaCore(); + return ( <> @@ -73,48 +64,62 @@ const HostsComponent = React.memo( filters, }); return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + <> + + + + - } - title={i18n.PAGE_TITLE} - /> - <> - - {({ kpiHosts, loading, id, inspect, refetch }) => ( - { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }} - /> - )} - - - } + title={i18n.PAGE_TITLE} /> - - - + <> + + {({ kpiHosts, loading, id, inspect, refetch }) => ( + { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }} + /> + )} + + + + + + + + ) : ( <> @@ -142,8 +147,12 @@ const makeMapStateToProps = () => { return mapStateToProps; }; +interface HostsProps extends GlobalTimeArgs { + hostsPagePath: string; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const Hosts = compose>( +export const Hosts = compose>( connect( makeMapStateToProps, { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx deleted file mode 100644 index 3d7e54b4a19ac..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getEsQueryConfig } from '@kbn/es-query'; -import React, { memo } from 'react'; -import { connect } from 'react-redux'; - -import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; -import { Anomaly } from '../../components/ml/types'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { convertToBuildEsQuery } from '../../lib/keury'; -import { useKibanaCore } from '../../lib/compose/kibana_core'; -import { hostsModel, inputsSelectors, State } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; - -import { HostsComponentProps } from './hosts'; -import { CommonChildren, AnomaliesChildren } from './navigation/types'; - -interface HostsBodyComponentProps extends HostsComponentProps { - children: CommonChildren | AnomaliesChildren; -} - -const HostsBodyComponent = memo( - ({ - children, - deleteQuery, - filters, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setQuery, - to, - }) => { - const core = useKibanaCore(); - return ( - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: getEsQueryConfig(core.uiSettings), - indexPattern, - queries: [query], - filters, - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - <> - {children({ - deleteQuery, - endDate: to, - filterQuery, - skip: isInitializing, - setQuery, - startDate: from, - type: hostsModel.HostsType.page, - indexPattern, - narrowDateRange: (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - updateDateRange: (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - })} - - ) : null; - }} - - ); - } -); - -HostsBodyComponent.displayName = 'HostsBodyComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const mapStateToProps = (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); - return mapStateToProps; -}; - -export const HostsBody = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, - } -)(HostsBodyComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx new file mode 100644 index 0000000000000..6dbfb422ed7a6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { HostsTabsProps } from './types'; +import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../components/ml/types'; +import { HostsTableType } from '../../store/hosts/model'; + +import { + HostsQueryTabBody, + AuthenticationsQueryTabBody, + UncommonProcessQueryTabBody, + AnomaliesQueryTabBody, + EventsQueryTabBody, +} from './navigation'; + +const HostsTabs = memo( + ({ + deleteQuery, + filterQuery, + setAbsoluteRangeDatePicker, + to, + from, + setQuery, + isInitializing, + type, + indexPattern, + hostsPagePath, + }) => { + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + skip: isInitializing, + setQuery, + startDate: from, + type, + indexPattern, + narrowDateRange: (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + updateDateRange: (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + }; + + return ( + + } + /> + } + /> + } + /> + } + /> + } + /> + + ); + } +); + +HostsTabs.displayName = 'HostsTabs'; + +export { HostsTabs }; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx index 6596d4c65c00e..c8d450a62cc57 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx @@ -7,21 +7,13 @@ import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { HostDetailsBody, HostDetails } from './details'; -import { - HostsQueryTabBody, - AuthenticationsQueryTabBody, - UncommonProcessQueryTabBody, - AnomaliesQueryTabBody, - EventsQueryTabBody, -} from './navigation'; -import { HostsBody } from './hosts_body'; +import { HostDetails } from './details'; import { HostsTableType } from '../../store/hosts/model'; + import { GlobalTime } from '../../containers/global_time'; import { SiemPageName } from '../home/types'; import { Hosts } from './hosts'; - -const hostsPagePath = `/:pageName(${SiemPageName.hosts})`; +import { hostsPagePath, hostDetailsPagePath } from './types'; const getHostsTabPath = (pagePath: string) => `${pagePath}/:tabName(` + @@ -32,7 +24,7 @@ const getHostsTabPath = (pagePath: string) => `${HostsTableType.events})`; const getHostDetailsTabPath = (pagePath: string) => - `${pagePath}/:detailName/:tabName(` + + `${hostDetailsPagePath}/:tabName(` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + `${HostsTableType.anomalies}|` + @@ -44,205 +36,49 @@ export const HostsContainer = React.memo(({ url }) => ( {({ to, from, setQuery, deleteQuery, isInitializing }) => ( - ( - ( - <> - - - - )} - /> - )} - /> ( - <> - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - + )} /> ( - <> - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - + )} /> ( - - )} + path={hostDetailsPagePath} + render={({ + match: { + params: { detailName }, + }, + location: { search = '' }, + }) => } /> ( - + )} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts new file mode 100644 index 0000000000000..980c5535129aa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { StaticIndexPattern } from 'ui/index_patterns'; +import { ActionCreator } from 'typescript-fsa'; +import { Filter } from '@kbn/es-query'; +import { Query } from 'src/plugins/data/common'; + +import { SiemPageName } from '../home/types'; +import { hostsModel } from '../../store'; +import { InputsModelId } from '../../store/inputs/constants'; +import { GlobalTimeArgs } from '../../containers/global_time'; + +export const hostsPagePath = `/:pageName(${SiemPageName.hosts})`; +export const hostDetailsPagePath = `${hostsPagePath}/:detailName`; + +export interface HostsComponentReduxProps { + query: Query; + filters: Filter[]; +} + +export interface HostsComponentDispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + hostsPagePath: string; +} + +export type HostsTabsProps = HostsComponentDispatchProps & + HostsQueryProps & { + filterQuery: string; + type: hostsModel.HostsType; + indexPattern: StaticIndexPattern; + }; + +export type HostsQueryProps = GlobalTimeArgs; + +export type HostsComponentProps = HostsComponentReduxProps & + HostsComponentDispatchProps & + HostsQueryProps; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx index b10c09d65426b..f7b3cfb4962fc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx @@ -14,9 +14,9 @@ import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; +import { SiemNavigation } from '../../components/navigation'; import { manageQuery } from '../../components/page/manage_query'; import { KpiNetworkComponent } from '../../components/page/network'; -import { SiemNavigation } from '../../components/navigation'; import { SiemSearchBar } from '../../components/search_bar'; import { KpiNetworkQuery } from '../../containers/kpi_network'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; @@ -26,7 +26,6 @@ import { convertToBuildEsQuery } from '../../lib/keury'; import { networkModel, State, inputsSelectors } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; - import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation'; import { NetworkEmptyPage } from './network_empty_page'; import * as i18n from './translations'; @@ -78,6 +77,8 @@ const NetworkComponent = React.memo( setQuery={setQuery} /> + + state.app.notesById; const getErrors = (state: State): ErrorModel => state.app.errors; -const getNotes = (notesById: NotesById, noteIds: string[]) => +export const getNotes = memoizeOne((notesById: NotesById, noteIds: string[]): Note[] => keys(notesById).reduce((acc: Note[], noteId: string) => { if (noteIds.includes(noteId)) { const note: Note = notesById[noteId]; return [...acc, note]; } return acc; - }, []); + }, []) +); export const selectNotesByIdSelector = createSelector( selectNotesById, @@ -34,8 +35,7 @@ export const selectNotesByIdSelector = createSelector( export const notesByIdsSelector = () => createSelector( selectNotesById, - (notesById: NotesById) => - memoizeOne((noteIds: string[]): Note[] => getNotes(notesById, noteIds)) + (notesById: NotesById) => notesById ); export const errorsSelector = () => diff --git a/x-pack/legacy/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap b/x-pack/legacy/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap new file mode 100644 index 0000000000000..50454fcb6b351 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap @@ -0,0 +1,1366 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Network Top N flow elasticsearch_adapter with FlowTarget=source Unhappy Path - No geo data getNetworkTopNFlow 1`] = ` +Object { + "edges": Array [ + Object { + "cursor": Object { + "tiebreaker": null, + "value": "1.1.1.1", + }, + "node": Object { + "_id": "1.1.1.1", + "network": Object { + "bytes_in": 11276023407, + "bytes_out": 1025631, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.1.net", + ], + "flows": 1234567, + "ip": "1.1.1.1", + "location": null, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "2.2.2.2", + }, + "node": Object { + "_id": "2.2.2.2", + "network": Object { + "bytes_in": 5469323342, + "bytes_out": 2811441, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.2.net", + ], + "flows": 1234567, + "ip": "2.2.2.2", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "3.3.3.3", + }, + "node": Object { + "_id": "3.3.3.3", + "network": Object { + "bytes_in": 3807671322, + "bytes_out": 4494034, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.3.com", + "test.3-duplicate.com", + ], + "flows": 1234567, + "ip": "3.3.3.3", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "4.4.4.4", + }, + "node": Object { + "_id": "4.4.4.4", + "network": Object { + "bytes_in": 166517626, + "bytes_out": 3194782, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.4.com", + ], + "flows": 1234567, + "ip": "4.4.4.4", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "5.5.5.5", + }, + "node": Object { + "_id": "5.5.5.5", + "network": Object { + "bytes_in": 104785026, + "bytes_out": 1838597, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.5.com", + ], + "flows": 1234567, + "ip": "5.5.5.5", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "6.6.6.6", + }, + "node": Object { + "_id": "6.6.6.6", + "network": Object { + "bytes_in": 28804250, + "bytes_out": 482982, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.6.com", + ], + "flows": 1234567, + "ip": "6.6.6.6", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "7.7.7.7", + }, + "node": Object { + "_id": "7.7.7.7", + "network": Object { + "bytes_in": 23032363, + "bytes_out": 400623, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.7.com", + ], + "flows": 1234567, + "ip": "7.7.7.7", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "8.8.8.8", + }, + "node": Object { + "_id": "8.8.8.8", + "network": Object { + "bytes_in": 21424889, + "bytes_out": 344357, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.8.com", + ], + "flows": 1234567, + "ip": "8.8.8.8", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "9.9.9.9", + }, + "node": Object { + "_id": "9.9.9.9", + "network": Object { + "bytes_in": 19205000, + "bytes_out": 355663, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.9.com", + ], + "flows": 1234567, + "ip": "9.9.9.9", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + Object { + "cursor": Object { + "tiebreaker": null, + "value": "10.10.10.10", + }, + "node": Object { + "_id": "10.10.10.10", + "network": Object { + "bytes_in": 11407633, + "bytes_out": 199360, + }, + "source": Object { + "autonomous_system": Object { + "name": "Level 3 Parent, LLC", + "number": 3356, + }, + "destination_ips": 345345, + "domain": Array [ + "test.10.com", + ], + "flows": 1234567, + "ip": "10.10.10.10", + "location": Object { + "flowTarget": "source", + "geo": Object { + "city_name": "Philadelphia", + "continent_name": "North America", + "country_iso_code": "US", + "location": Object { + "lat": 39.9359, + "lon": -75.1534, + }, + "region_iso_code": "US-PA", + "region_name": "Pennsylvania", + }, + }, + }, + }, + }, + ], + "inspect": Object { + "dsl": Array [ + "{ + \\"mockTopNFlowQueryDsl\\": \\"mockTopNFlowQueryDsl\\" +}", + ], + "response": Array [ + "{ + \\"took\\": 122, + \\"timed_out\\": false, + \\"_shards\\": { + \\"total\\": 11, + \\"successful\\": 11, + \\"skipped\\": 0, + \\"failed\\": 0 + }, + \\"hits\\": { + \\"max_score\\": null, + \\"hits\\": [] + }, + \\"aggregations\\": { + \\"top_n_flow_count\\": { + \\"value\\": 545 + }, + \\"source\\": { + \\"buckets\\": [ + { + \\"key\\": \\"1.1.1.1\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 11276023407 + }, + \\"bytes_out\\": { + \\"value\\": 1025631 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.1.net\\" + } + ] + } + }, + { + \\"key\\": \\"2.2.2.2\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 5469323342 + }, + \\"bytes_out\\": { + \\"value\\": 2811441 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.2.net\\" + } + ] + } + }, + { + \\"key\\": \\"3.3.3.3\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 3807671322 + }, + \\"bytes_out\\": { + \\"value\\": 4494034 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.3.com\\" + }, + { + \\"key\\": \\"test.3-duplicate.com\\" + } + ] + } + }, + { + \\"key\\": \\"4.4.4.4\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 166517626 + }, + \\"bytes_out\\": { + \\"value\\": 3194782 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.4.com\\" + } + ] + } + }, + { + \\"key\\": \\"5.5.5.5\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 104785026 + }, + \\"bytes_out\\": { + \\"value\\": 1838597 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.5.com\\" + } + ] + } + }, + { + \\"key\\": \\"6.6.6.6\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 28804250 + }, + \\"bytes_out\\": { + \\"value\\": 482982 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"doc_count_error_upper_bound\\": 0, + \\"sum_other_doc_count\\": 31, + \\"buckets\\": [ + { + \\"key\\": \\"test.6.com\\" + } + ] + } + }, + { + \\"key\\": \\"7.7.7.7\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 23032363 + }, + \\"bytes_out\\": { + \\"value\\": 400623 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"doc_count_error_upper_bound\\": 0, + \\"sum_other_doc_count\\": 0, + \\"buckets\\": [ + { + \\"key\\": \\"test.7.com\\" + } + ] + } + }, + { + \\"key\\": \\"8.8.8.8\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 21424889 + }, + \\"bytes_out\\": { + \\"value\\": 344357 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.8.com\\" + } + ] + } + }, + { + \\"key\\": \\"9.9.9.9\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 19205000 + }, + \\"bytes_out\\": { + \\"value\\": 355663 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.9.com\\" + } + ] + } + }, + { + \\"key\\": \\"10.10.10.10\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 11407633 + }, + \\"bytes_out\\": { + \\"value\\": 199360 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.10.com\\" + } + ] + } + }, + { + \\"key\\": \\"11.11.11.11\\", + \\"flows\\": { + \\"value\\": 1234567 + }, + \\"destination_ips\\": { + \\"value\\": 345345 + }, + \\"bytes_in\\": { + \\"value\\": 11393327 + }, + \\"bytes_out\\": { + \\"value\\": 195914 + }, + \\"location\\": { + \\"doc_count\\": 14, + \\"top_geo\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"geo\\": { + \\"continent_name\\": \\"North America\\", + \\"region_iso_code\\": \\"US-PA\\", + \\"city_name\\": \\"Philadelphia\\", + \\"country_iso_code\\": \\"US\\", + \\"region_name\\": \\"Pennsylvania\\", + \\"location\\": { + \\"lon\\": -75.1534, + \\"lat\\": 39.9359 + } + } + } + } + } + ] + } + } + }, + \\"autonomous_system\\": { + \\"doc_count\\": 14, + \\"top_as\\": { + \\"hits\\": { + \\"total\\": { + \\"value\\": 14, + \\"relation\\": \\"eq\\" + }, + \\"max_score\\": 1, + \\"hits\\": [ + { + \\"_index\\": \\"filebeat-8.0.0-2019.06.19-000005\\", + \\"_type\\": \\"_doc\\", + \\"_id\\": \\"dd4fa2d4bd-692279846149410\\", + \\"_score\\": 1, + \\"_source\\": { + \\"source\\": { + \\"as\\": { + \\"number\\": 3356, + \\"organization\\": { + \\"name\\": \\"Level 3 Parent, LLC\\" + } + } + } + } + } + ] + } + } + }, + \\"domain\\": { + \\"buckets\\": [ + { + \\"key\\": \\"test.11.com\\" + } + ] + } + } + ] + } + } +}", + ], + }, + "pageInfo": Object { + "activePage": 0, + "fakeTotalCount": 50, + "showMorePagesIndicator": true, + }, + "totalCount": 545, +} +`; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts index c3bcfafac8757..542a2a0108a9a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts @@ -96,6 +96,36 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = }); }); + describe('Unhappy Path - No geo data', () => { + const mockCallWithRequest = jest.fn(); + const mockNoGeoDataResponse = cloneDeep(mockResponse); + // sometimes bad things happen to good ecs + mockNoGeoDataResponse.aggregations[ + FlowTargetSourceDest.source + ].buckets[0].location.top_geo.hits.hits = []; + mockCallWithRequest.mockResolvedValue(mockNoGeoDataResponse); + const mockFramework: FrameworkAdapter = { + version: 'mock', + callWithRequest: mockCallWithRequest, + exposeStaticDir: jest.fn(), + getIndexPatternsService: jest.fn(), + getSavedObjectsService: jest.fn(), + registerGraphQLEndpoint: jest.fn(), + }; + jest.doMock('../framework', () => ({ + callWithRequest: mockCallWithRequest, + })); + + test('getNetworkTopNFlow', async () => { + const EsNetworkTopNFlow = new ElasticsearchNetworkAdapter(mockFramework); + const data: NetworkTopNFlowData = await EsNetworkTopNFlow.getNetworkTopNFlow( + mockRequest as FrameworkRequest, + mockOptions + ); + expect(data).toMatchSnapshot(); + }); + }); + describe('No pagination', () => { const mockNoPaginationResponse = cloneDeep(mockResponse); mockNoPaginationResponse.aggregations.top_n_flow_count.value = 10; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts index 5a871a3f9c9b4..eff5fba0c54d5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts @@ -193,19 +193,20 @@ const getGeoItem = (result: NetworkTopNFlowBuckets): GeoItem | null => : null; const getAsItem = (result: NetworkTopNFlowBuckets): AutonomousSystemItem | null => - result.autonomous_system.top_as.hits.hits.length > 0 + result.autonomous_system.top_as.hits.hits.length > 0 && + result.autonomous_system.top_as.hits.hits[0]._source ? { number: getOr( null, `autonomous_system.top_as.hits.hits[0]._source.${ - Object.keys(result.location.top_geo.hits.hits[0]._source)[0] + Object.keys(result.autonomous_system.top_as.hits.hits[0]._source)[0] }.as.number`, result ), name: getOr( '', `autonomous_system.top_as.hits.hits[0]._source.${ - Object.keys(result.location.top_geo.hits.hits[0]._source)[0] + Object.keys(result.autonomous_system.top_as.hits.hits[0]._source)[0] }.as.organization.name`, result ), diff --git a/x-pack/legacy/plugins/task_manager/task_manager.test.ts b/x-pack/legacy/plugins/task_manager/task_manager.test.ts index 2bcfdb2f4d4e3..8592ff31d700f 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.test.ts @@ -7,11 +7,11 @@ import _ from 'lodash'; import sinon from 'sinon'; import { TaskManager, claimAvailableTasks } from './task_manager'; -import { SavedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; import { SavedObjectsSerializer, SavedObjectsSchema } from 'src/core/server'; import { mockLogger } from './test_utils'; -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); describe('TaskManager', () => { diff --git a/x-pack/legacy/plugins/task_manager/task_store.test.ts b/x-pack/legacy/plugins/task_manager/task_store.test.ts index 65b49820d6e6c..9779dc5efd28b 100644 --- a/x-pack/legacy/plugins/task_manager/task_store.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_store.test.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { TaskDictionary, TaskDefinition, TaskInstance, TaskStatus } from './task'; import { FetchOpts, StoreOpts, OwnershipClaimingOpts, TaskStore } from './task_store'; import { mockLogger } from './test_utils'; -import { SavedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; import { SavedObjectsSerializer, SavedObjectsSchema, SavedObjectAttributes } from 'src/core/server'; const taskDefinitions: TaskDictionary = { @@ -31,7 +31,7 @@ const taskDefinitions: TaskDictionary = { }, }; -const savedObjectsClient = SavedObjectsClientMock.create(); +const savedObjectsClient = savedObjectsClientMock.create(); const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); beforeEach(() => jest.resetAllMocks()); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index e7f79b240d81a..65dd8a34330a5 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -142,7 +142,7 @@ export const PopoverForm: React.SFC = ({ {isUnsupportedAgg && ( = ({ = React.memo( try { const resp = await api.createTransform(transformId, transformConfig); - - if (resp.errors !== undefined) { - if (Array.isArray(resp.errors) && resp.errors.length === 1) { + if (resp.errors !== undefined && Array.isArray(resp.errors)) { + if (resp.errors.length === 1) { throw resp.errors[0]; } - throw resp.errors; + if (resp.errors.length > 1) { + throw resp.errors; + } } toastNotifications.addSuccess( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 49017ac9e3c02..6cde57fc2316d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -591,6 +591,7 @@ export const StepDefineForm: SFC = React.memo(({ overrides = {}, onChange setOptions={{ fontSize: '12px', }} + theme="textmate" aria-label={i18n.translate( 'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', { @@ -751,6 +752,7 @@ export const StepDefineForm: SFC = React.memo(({ overrides = {}, onChange setOptions={{ fontSize: '12px', }} + theme="textmate" aria-label={i18n.translate( 'xpack.transform.stepDefineForm.advancedEditorAriaLabel', { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap index f185579fd9e4c..0d4a80a94ee51 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap @@ -15,6 +15,7 @@ exports[`Transform: Transform List Expanded Row Minimal "width": "100%", } } + theme="textmate" value="{ \\"id\\": \\"fq_date_histogram_1m_1441\\", \\"source\\": { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx index d24fc19f216a3..416d93007daba 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx @@ -28,6 +28,7 @@ export const ExpandedRowJsonPane: SFC = ({ json }) => { readOnly={true} mode="json" style={{ width: '100%' }} + theme="textmate" />   diff --git a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts b/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts index 1e691b45e1792..f291450ab2a7a 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +const MINUTE = 1000 * 60; +const HOUR = MINUTE * 60; const DAY = 24 * 60 * 60 * 1000; const WEEK = DAY * 7; +const MONTH = WEEK * 4; /** * These contsants are used by the charting code to determine @@ -14,9 +17,10 @@ const WEEK = DAY * 7; */ export const CHART_FORMAT_LIMITS = { DAY, - FIFTEEN_DAYS: 1000 * 60 * 60 * 24 * 15, - EIGHT_MINUTES: 1000 * 60 * 8, + EIGHT_MINUTES: MINUTE * 8, FOUR_YEARS: 4 * 12 * 4 * WEEK, - THIRTY_SIX_HOURS: 1000 * 60 * 60 * 36, + THIRTY_SIX_HOURS: HOUR * 36, THREE_WEEKS: WEEK * 3, + SIX_MONTHS: MONTH * 7, + NINE_DAYS: DAY * 9, }; diff --git a/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts b/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts index 7f5699eb7e8a4..296df279b8eec 100644 --- a/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts +++ b/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HistogramDataPoint } from '../graphql/types'; + export interface UMGqlRange { dateRangeStart: string; dateRangeEnd: string; } + +export interface HistogramResult { + histogram: HistogramDataPoint[]; + interval: number; +} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx index 3ae6f349d3997..76c3bd5d4c50d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx @@ -25,6 +25,10 @@ export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { colors: { danger, gray }, } = useContext(UptimeSettingsContext); + let upCount = up; + if (up === 0 && down === 0) { + upCount = 1; + } useEffect(() => { if (chartElement.current !== null) { // we must remove any existing paths before painting @@ -50,7 +54,7 @@ export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { .data( // @ts-ignore pie generator expects param of type number[], but only works with // output of d3.entries, which is like Array<{ key: string, value: number }> - pieGenerator(d3.entries({ up, down })) + pieGenerator(d3.entries({ up: upCount, down })) ) .enter() .append('path') @@ -64,7 +68,7 @@ export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { ) .attr('fill', (d: any) => color(d.data.key)); } - }, [chartElement.current, up, down]); + }, [chartElement.current, upCount, down]); return ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx index e39adc2868c84..a5d2d9cfc27f2 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx @@ -20,13 +20,13 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import styled from 'styled-components'; -import { HistogramDataPoint } from '../../../../common/graphql/types'; import { getColorsMap } from './get_colors_map'; import { getChartDateLabel } from '../../../lib/helper'; import { withUptimeGraphQL, UptimeGraphQLQueryProps } from '../../higher_order'; import { snapshotHistogramQuery } from '../../../queries/snapshot_histogram_query'; import { ChartWrapper } from './chart_wrapper'; import { UptimeSettingsContext } from '../../../contexts'; +import { HistogramResult } from '../../../../common/domain_types'; const SnapshotHistogramWrapper = styled.div` margin-left: 120px; @@ -56,7 +56,7 @@ export interface SnapshotHistogramProps { } interface SnapshotHistogramQueryResult { - histogram?: HistogramDataPoint[]; + queryResult?: HistogramResult; } type Props = UptimeGraphQLQueryProps & SnapshotHistogramProps; @@ -68,7 +68,7 @@ export const SnapshotHistogramComponent = ({ loading = false, height, }: Props) => { - if (!data || !data.histogram) + if (!data || !data.queryResult) /** * TODO: the Fragment, EuiTitle, and EuiPanel should be extracted to a dumb component * that we can reuse in the subsequent return statement at the bottom of this function. @@ -107,7 +107,9 @@ export const SnapshotHistogramComponent = ({ ); - const { histogram } = data; + const { + queryResult: { histogram, interval }, + } = data; const { colors: { danger, gray }, @@ -145,7 +147,14 @@ export const SnapshotHistogramComponent = ({ })} > - + setSearchQuery(query)} placeholder={ isLoading @@ -98,6 +100,7 @@ export const FilterPopover = ({ {item} ))} + {id === 'location' && items.length === 0 && } ); }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts index c435e5acc2f28..bdf0832a98fdf 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts @@ -24,7 +24,7 @@ describe('getChartLabelFormatter', () => { it('creates a label with month/day and hour/minute for time between 36 hours and 4 days', () => { // Sun, 15 Jul 2001 17:53:10 GMT -> Thu, 19 Jul 2001 17:52:59 GMT - expect(getChartDateLabel(995219590000, 995565179000)).toBe('MM-dd HH:mm'); + expect(getChartDateLabel(995219590000, 995565179000)).toBe('MM-DD HH:mm'); }); it('creates a format without day/month string for delta within same day local time', () => { @@ -34,17 +34,25 @@ describe('getChartLabelFormatter', () => { it('creates a format with date/month string for delta crossing dates', () => { // Wed, 18 Jul 2001 11:06:19 GMT -> Thu, 19 Jul 2001 17:52:59 GMT - expect(getChartDateLabel(995454379000, 995565179000)).toBe('MM-dd HH:mm'); + expect(getChartDateLabel(995454379000, 995565179000)).toBe('MM-DD HH:mm'); }); it('creates a format with only month/day for delta between to eight days and two weeks', () => { // Sun, 01 Jul 2001 23:28:15 GMT -> Thu, 19 Jul 2001 17:52:59 GMT - expect(getChartDateLabel(994030095000, 995565179000)).toBe('MM-dd'); + expect(getChartDateLabel(994030095000, 995565179000)).toBe('MM-DD'); }); - it('creates a format with the year/month for range exceeding a week', () => { + it('creates a format with the date and hour/min for 36h < range < 9d', () => { + expect(getChartDateLabel(1003752000000, 1004270400000)).toBe('MM-DD HH:mm'); + }); + + it('creates a format with month/day for range between nine days and three weeks', () => { + expect(getChartDateLabel(1003752000000, 1004875200000)).toBe('MM-DD'); + }); + + it('creates a format with the year/month/day for range exceeding three weeks', () => { // Sun, 15 Jul 2001 12:27:59 GMT -> Fri, 28 Dec 2001 18:46:19 GMT - expect(getChartDateLabel(995200079000, 1009565179000)).toBe('yyyy-MM'); + expect(getChartDateLabel(995200079000, 1009565179000)).toBe('YYYY-MM-DD'); }); it('creates a format of only year for timespan > 4 years', () => { diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts index 3533f2667b66c..126b1d85f749f 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts @@ -34,7 +34,7 @@ export const getChartDateLabel = (dateRangeStart: number, dateRangeEnd: number) delta < CHART_FORMAT_LIMITS.THIRTY_SIX_HOURS && !isWithinCurrentDate(dateRangeStart, dateRangeEnd) ) { - formatString = 'MM-dd '; + formatString = 'MM-DD '; } return formatString + getLabelFormat(delta); }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts index ad3e04f403640..668147fee8055 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts @@ -7,11 +7,12 @@ import { CHART_FORMAT_LIMITS } from '../../../../common/constants'; const { - FIFTEEN_DAYS, EIGHT_MINUTES, FOUR_YEARS, THIRTY_SIX_HOURS, THREE_WEEKS, + SIX_MONTHS, + NINE_DAYS, } = CHART_FORMAT_LIMITS; /** @@ -30,16 +31,20 @@ const dateStops: Array<{ key: number; value: string }> = [ value: 'HH:mm', }, { - key: FIFTEEN_DAYS, - value: 'MM-dd HH:mm', + key: NINE_DAYS, + value: 'MM-DD HH:mm', }, { key: THREE_WEEKS, - value: 'MM-dd', + value: 'MM-DD', + }, + { + key: SIX_MONTHS, + value: 'YYYY-MM-DD', }, { key: FOUR_YEARS, - value: 'yyyy-MM', + value: 'YYYY-MM', }, ]; diff --git a/x-pack/legacy/plugins/uptime/public/queries/snapshot_histogram_query.ts b/x-pack/legacy/plugins/uptime/public/queries/snapshot_histogram_query.ts index 37f7178d23630..7eb56ea4e9dd1 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/snapshot_histogram_query.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/snapshot_histogram_query.ts @@ -14,18 +14,21 @@ export const snapshotHistogramQueryString = ` $monitorId: String $statusFilter: String ) { - histogram: getSnapshotHistogram( + queryResult: getSnapshotHistogram( dateRangeStart: $dateRangeStart dateRangeEnd: $dateRangeEnd filters: $filters statusFilter: $statusFilter monitorId: $monitorId ) { + histogram { upCount downCount x x0 y + } + interval } } `; diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts index 287e80a6291ca..96a386b6a6848 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -17,11 +17,11 @@ import { MonitorPageTitle, Ping, Snapshot, - HistogramDataPoint, GetSnapshotHistogramQueryArgs, } from '../../../common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { CreateUMGraphQLResolvers, UMContext } from '../types'; +import { HistogramResult } from '../../../common/domain_types'; export type UMSnapshotResolver = UMResolver< Snapshot | Promise, @@ -61,7 +61,7 @@ export type UMGetMontiorPageTitleResolver = UMResolver< >; export type UMGetSnapshotHistogram = UMResolver< - HistogramDataPoint[] | Promise, + HistogramResult | Promise, any, GetSnapshotHistogramQueryArgs, UMContext @@ -101,7 +101,7 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( resolver, { dateRangeStart, dateRangeEnd, filters, monitorId, statusFilter }, { req } - ): Promise { + ): Promise { return await libs.pings.getPingHistogram( req, dateRangeStart, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts index d406be2e8b15b..97dcbd12fff2e 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts @@ -120,6 +120,11 @@ export const monitorsSchema = gql` monitors: [LatestMonitor!] } + type HistogramResult { + histogram: [HistogramDataPoint]! + interval: UnsignedInteger! + } + type MonitorPageTitle { id: String! url: String @@ -147,7 +152,7 @@ export const monitorsSchema = gql` filters: String statusFilter: String monitorId: String - ): [HistogramDataPoint!]! + ): HistogramResult getMonitorChartsData( monitorId: String! diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts index 2518264292cbd..6b594d8b49118 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts @@ -6,7 +6,7 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from '../elasticsearch_monitor_states_adapter'; -import { getHistogramInterval } from '../../../helper'; +import { getHistogramIntervalFormatted } from '../../../helper'; import { INDEX_NAMES, STATES } from '../../../../../common/constants'; import { MonitorSummary, @@ -324,7 +324,7 @@ const getHistogramForMonitors = async ( histogram: { date_histogram: { field: '@timestamp', - fixed_interval: getHistogramInterval( + fixed_interval: getHistogramIntervalFormatted( queryContext.dateRangeStart, queryContext.dateRangeEnd ), diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/elasticsearch_monitors_adapter.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/elasticsearch_monitors_adapter.test.ts.snap index 75b19d7381a62..99349f42d5750 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/elasticsearch_monitors_adapter.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/elasticsearch_monitors_adapter.test.ts.snap @@ -221,6 +221,10 @@ Object { "x": 1568412432000, "y": 1972483.25, }, + Object { + "x": 1568412468000, + "y": 1020490, + }, ], "name": "us-east-2", }, @@ -302,6 +306,10 @@ Object { "x": 1568412432000, "y": 1543307.5, }, + Object { + "x": 1568412468000, + "y": null, + }, ], "name": "us-west-4", }, @@ -421,6 +429,12 @@ Object { "up": null, "x": 1568412432000, }, + Object { + "down": null, + "total": 1, + "up": null, + "x": 1568412468000, + }, ], "statusMaxCount": 0, } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index c14e3dab987d7..f2d84d149344b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -13,12 +13,7 @@ import { Ping, LocationDurationLine, } from '../../../../common/graphql/types'; -import { - dropLatestBucket, - getFilterClause, - getHistogramInterval, - parseFilterQuery, -} from '../../helper'; +import { getFilterClause, parseFilterQuery, getHistogramIntervalFormatted } from '../../helper'; import { DatabaseAdapter } from '../database'; import { UMMonitorsAdapter } from './adapter_types'; @@ -80,7 +75,7 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { timeseries: { date_histogram: { field: '@timestamp', - fixed_interval: getHistogramInterval(dateRangeStart, dateRangeEnd), + fixed_interval: getHistogramIntervalFormatted(dateRangeStart, dateRangeEnd), min_doc_count: 0, }, aggs: { @@ -102,10 +97,7 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { const result = await this.database.search(request, params); - const dateHistogramBuckets = dropLatestBucket( - get(result, 'aggregations.timeseries.buckets', []) - ); - + const dateHistogramBuckets = get(result, 'aggregations.timeseries.buckets', []); /** * The code below is responsible for formatting the aggregation data we fetched above in a way * that the chart components used by the client understands. diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap index a7526739c95ac..b73595d539e93 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap @@ -1,82 +1,127 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ElasticsearchPingsAdapter class getPingHistogram handles simple_text_query without issues 1`] = ` -Array [ - Object { - "downCount": 1, - "key": 1, - "upCount": 2, - "x": 2, - "x0": 1, - "y": 1, - }, - Object { - "downCount": 2, - "key": 2, - "upCount": 1, - "x": 3, - "x0": 2, - "y": 1, - }, -] +Object { + "histogram": Array [ + Object { + "downCount": 1, + "upCount": 2, + "x": 1, + "y": 1, + }, + Object { + "downCount": 2, + "upCount": 1, + "x": 2, + "y": 1, + }, + Object { + "downCount": 1, + "upCount": 3, + "x": 3, + "y": 1, + }, + ], + "interval": 36000, +} `; exports[`ElasticsearchPingsAdapter class getPingHistogram handles status + additional user queries 1`] = ` -Array [ - Object { - "downCount": 1, - "key": 1, - "upCount": 0, - "x": 2, - "x0": 1, - "y": 1, - }, - Object { - "downCount": 2, - "key": 2, - "upCount": 0, - "x": 3, - "x0": 2, - "y": 1, - }, -] +Object { + "histogram": Array [ + Object { + "downCount": 1, + "upCount": 0, + "x": 1, + "y": 1, + }, + Object { + "downCount": 2, + "upCount": 0, + "x": 2, + "y": 1, + }, + Object { + "downCount": 1, + "upCount": 0, + "x": 3, + "y": 1, + }, + ], + "interval": 5609564928000, +} `; exports[`ElasticsearchPingsAdapter class getPingHistogram returns a down-filtered array for when filtered by down status 1`] = ` -Array [ - Object { - "downCount": 1, - "key": 1, - "upCount": 0, - "x": 2, - "x0": 1, - "y": 1, - }, -] +Object { + "histogram": Array [ + Object { + "downCount": 1, + "upCount": 0, + "x": 1, + "y": 1, + }, + Object { + "downCount": undefined, + "upCount": 0, + "x": 2, + "y": 1, + }, + ], + "interval": 5609564928000, +} `; exports[`ElasticsearchPingsAdapter class getPingHistogram returns a down-filtered array for when filtered by up status 1`] = ` -Array [ - Object { - "downCount": 0, - "key": 1, - "upCount": 2, - "x": 2, - "x0": 1, - "y": 1, - }, -] +Object { + "histogram": Array [ + Object { + "downCount": 0, + "upCount": 2, + "x": 1, + "y": 1, + }, + Object { + "downCount": 0, + "upCount": 2, + "x": 2, + "y": 1, + }, + ], + "interval": 5609564928000, +} +`; + +exports[`ElasticsearchPingsAdapter class getPingHistogram returns a single bucket if array has 1 1`] = ` +Object { + "histogram": Array [ + Object { + "downCount": 1, + "upCount": 2, + "x": 1, + "y": 1, + }, + ], + "interval": 36000, +} `; exports[`ElasticsearchPingsAdapter class getPingHistogram returns expected result for no status filter 1`] = ` -Array [ - Object { - "downCount": 1, - "key": 1, - "upCount": 2, - "x": 2, - "x0": 1, - "y": 1, - }, -] +Object { + "histogram": Array [ + Object { + "downCount": 1, + "upCount": 2, + "x": 1, + "y": 1, + }, + Object { + "downCount": undefined, + "upCount": 2, + "x": 2, + "y": 1, + }, + ], + "interval": 36000, +} `; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts index ec414cda7d811..5c481cd147c61 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts @@ -88,7 +88,7 @@ describe('ElasticsearchPingsAdapter class', () => { }); describe('getPingHistogram', () => { - it('returns an empty array for <= 1 bucket', async () => { + it('returns a single bucket if array has 1', async () => { expect.assertions(2); const search = jest.fn(); search.mockReturnValue({ @@ -116,7 +116,7 @@ describe('ElasticsearchPingsAdapter class', () => { const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); const result = await pingAdapter.getPingHistogram(serverRequest, 'now-15m', 'now', null); expect(pingDatabase.search).toHaveBeenCalledTimes(1); - expect(result).toEqual([]); + expect(result).toMatchSnapshot(); }); it('returns expected result for no status filter', async () => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts index bb26e04f2fc9e..1e0cf7ec40646 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/adapter_types.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DocCount, HistogramDataPoint, Ping, PingResults } from '../../../../common/graphql/types'; +import { DocCount, Ping, PingResults } from '../../../../common/graphql/types'; +import { HistogramResult } from '../../../../common/domain_types'; export interface UMPingsAdapter { getAll( @@ -33,7 +34,7 @@ export interface UMPingsAdapter { filters?: string | null, monitorId?: string | null, statusFilter?: string | null - ): Promise; + ): Promise; getDocCount(request: any): Promise; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts index 822441449a774..cad8b412f3e58 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/elasticsearch_pings_adapter.ts @@ -6,17 +6,12 @@ import { get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; -import { - DocCount, - HistogramDataPoint, - HttpBody, - Ping, - PingResults, -} from '../../../../common/graphql/types'; -import { formatEsBucketsForHistogram, parseFilterQuery, getFilterClause } from '../../helper'; +import { DocCount, HttpBody, Ping, PingResults } from '../../../../common/graphql/types'; +import { parseFilterQuery, getFilterClause, getHistogramIntervalFormatted } from '../../helper'; import { DatabaseAdapter, HistogramQueryResult } from '../database'; import { UMPingsAdapter } from './adapter_types'; import { getHistogramInterval } from '../../helper/get_histogram_interval'; +import { HistogramResult } from '../../../../common/domain_types'; export class ElasticsearchPingsAdapter implements UMPingsAdapter { private database: DatabaseAdapter; @@ -195,7 +190,7 @@ export class ElasticsearchPingsAdapter implements UMPingsAdapter { filters?: string | null, monitorId?: string | null, statusFilter?: string | null - ): Promise { + ): Promise { const boolFilters = parseFilterQuery(filters); const additionaFilters = []; if (monitorId) { @@ -205,6 +200,8 @@ export class ElasticsearchPingsAdapter implements UMPingsAdapter { additionaFilters.push(boolFilters); } const filter = getFilterClause(dateRangeStart, dateRangeEnd, additionaFilters); + const interval = getHistogramInterval(dateRangeStart, dateRangeEnd); + const intervalFormatted = getHistogramIntervalFormatted(dateRangeStart, dateRangeEnd); const params = { index: INDEX_NAMES.HEARTBEAT, @@ -219,7 +216,7 @@ export class ElasticsearchPingsAdapter implements UMPingsAdapter { timeseries: { date_histogram: { field: '@timestamp', - fixed_interval: getHistogramInterval(dateRangeStart, dateRangeEnd), + fixed_interval: intervalFormatted, }, aggs: { down: { @@ -244,19 +241,21 @@ export class ElasticsearchPingsAdapter implements UMPingsAdapter { const result = await this.database.search(request, params); const buckets: HistogramQueryResult[] = get(result, 'aggregations.timeseries.buckets', []); - const mappedBuckets = buckets.map(bucket => { - const key: number = get(bucket, 'key'); + const histogram = buckets.map(bucket => { + const x: number = get(bucket, 'key'); const downCount: number = get(bucket, 'down.doc_count'); const upCount: number = get(bucket, 'up.doc_count'); return { - key, + x, downCount: statusFilter && statusFilter !== 'down' ? 0 : downCount, upCount: statusFilter && statusFilter !== 'up' ? 0 : upCount, y: 1, }; }); - - return formatEsBucketsForHistogram(mappedBuckets); + return { + histogram, + interval, + }; } /** diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/drop_latest_buckets.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/drop_latest_buckets.test.ts.snap deleted file mode 100644 index db05bb02be8a9..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/drop_latest_buckets.test.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dropLatestBucket drops the last of a list with greater length than 1 1`] = ` -Array [ - Object { - "prop": "val", - }, -] -`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/format_es_buckets_for_histogram.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/format_es_buckets_for_histogram.test.ts.snap deleted file mode 100644 index 1b30ee1549273..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/format_es_buckets_for_histogram.test.ts.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`formatEsBucketsForHistogram returns properly formatted buckets 1`] = ` -Array [ - Object { - "key": 1000, - "x": 2000, - "x0": 1000, - }, - Object { - "key": 2000, - "x": 3000, - "x0": 2000, - }, - Object { - "key": 3000, - "x": 4000, - "x0": 3000, - }, -] -`; - -exports[`formatEsBucketsForHistogram returns properly formatted object for generic call 1`] = ` -Array [ - Object { - "key": 1000, - "name": "something", - "value": 150, - "x": 2000, - "x0": 1000, - }, - Object { - "key": 2000, - "name": "something", - "value": 120, - "x": 3000, - "x0": 2000, - }, -] -`; - -exports[`formatEsBucketsForHistogram returns the provided buckets if length is below min threshold 1`] = `Array []`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/drop_latest_buckets.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/drop_latest_buckets.test.ts deleted file mode 100644 index 57ae48f0c7b63..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/drop_latest_buckets.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { dropLatestBucket } from '../drop_latest_bucket'; - -describe('dropLatestBucket', () => { - it('drops the last of a list with greater length than 1', () => { - const testData = [{ prop: 'val' }, { prop: 'val' }]; - const result = dropLatestBucket(testData); - expect(result).toMatchSnapshot(); - }); - it('returns an empty list when length === 1', () => { - const testData = [{ prop: 'val' }]; - const result = dropLatestBucket(testData); - expect(result).toEqual([]); - }); - it('returns an empty list when length === 0', () => { - const testData: any[] = []; - const result = dropLatestBucket(testData); - expect(result).toEqual([]); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/format_es_buckets_for_histogram.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/format_es_buckets_for_histogram.test.ts deleted file mode 100644 index 87c6aad5da032..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/format_es_buckets_for_histogram.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { formatEsBucketsForHistogram } from '../format_es_buckets_for_histogram'; - -describe('formatEsBucketsForHistogram', () => { - it('returns the provided buckets if length is below min threshold', () => { - const buckets = [{ key: 1000 }]; - const result = formatEsBucketsForHistogram(buckets); - expect(result).toMatchSnapshot(); - }); - it('returns properly formatted buckets', () => { - const buckets = [{ key: 1000 }, { key: 2000 }, { key: 3000 }, { key: 4000 }]; - const result = formatEsBucketsForHistogram(buckets); - expect(result).toMatchSnapshot(); - }); - it('returns properly formatted object for generic call', () => { - const buckets = [ - { key: 1000, name: 'something', value: 150 }, - { key: 2000, name: 'something', value: 120 }, - { key: 3000, name: 'something', value: 180 }, - ]; - const result = formatEsBucketsForHistogram(buckets); - expect(result).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts index 83f861d3fb467..bddca1b863ce4 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts @@ -5,29 +5,16 @@ */ import { getHistogramInterval } from '../get_histogram_interval'; +import { assertCloseTo } from '../assert_close_to'; describe('getHistogramInterval', () => { it('specifies the interval necessary to divide a given timespan into equal buckets, rounded to the nearest integer, expressed in ms', () => { - const result = getHistogramInterval('now-15m', 'now', 10); - /** - * These assertions were verbatim comparisons but that introduced - * some flakiness at the ms resolution, sometimes values like "9001ms" - * are returned. - */ - expect(result.startsWith('9000')).toBeTruthy(); - expect(result.endsWith('ms')).toBeTruthy(); - expect(result).toHaveLength(7); + const interval = getHistogramInterval('now-15m', 'now', 10); + assertCloseTo(interval, 90000, 10); }); it('will supply a default constant value for bucketCount when none is provided', () => { - const result = getHistogramInterval('now-15m', 'now'); - /** - * These assertions were verbatim comparisons but that introduced - * some flakiness at the ms resolution, sometimes values like "9001ms" - * are returned. - */ - expect(result.startsWith('3600')).toBeTruthy(); - expect(result.endsWith('ms')).toBeTruthy(); - expect(result).toHaveLength(7); + const interval = getHistogramInterval('now-15m', 'now'); + assertCloseTo(interval, 36000, 10); }); }); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts new file mode 100644 index 0000000000000..e67a93f24b3ca --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getHistogramIntervalFormatted } from '../get_histogram_interval_formatted'; + +describe('getHistogramIntervalFormatted', () => { + it('specifies the interval necessary to divide a given timespan into equal buckets, rounded to the nearest integer, expressed in ms', () => { + const intervalFormatted = getHistogramIntervalFormatted('now-15m', 'now', 10); + /** + * Expected result is 90000. + * These assertions were verbatim comparisons but that introduced + * some flakiness at the ms resolution, sometimes values like "9001ms" + * are returned. + */ + expect(intervalFormatted.startsWith('9000')).toBeTruthy(); + expect(intervalFormatted.endsWith('ms')).toBeTruthy(); + expect(intervalFormatted).toHaveLength(7); + }); + + it('will supply a default constant value for bucketCount when none is provided', () => { + const intervalFormatted = getHistogramIntervalFormatted('now-15m', 'now'); + /** + * Expected result is 36000. + * These assertions were verbatim comparisons but that introduced + * some flakiness at the ms resolution, sometimes values like "9001ms" + * are returned. + */ + expect(intervalFormatted.startsWith('3600')).toBeTruthy(); + expect(intervalFormatted.endsWith('ms')).toBeTruthy(); + expect(intervalFormatted).toHaveLength(7); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/drop_latest_bucket.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/drop_latest_bucket.ts deleted file mode 100644 index 4d072266fa4ea..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/drop_latest_bucket.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * We've had numerous requests to not display semi-full buckets (i.e. it is 13:01 and the - * bounds of our bucket are 13:00-13:05). If the first bucket isn't done filling, we'll - * start out with nothing returned, otherwise we drop the most recent bucket. - * @param buckets The bucket list - */ -export const dropLatestBucket = (buckets: any[]) => - buckets.length > 1 ? buckets.slice(0, buckets.length - 1) : []; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/format_es_buckets_for_histogram.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/format_es_buckets_for_histogram.ts deleted file mode 100644 index 1b0a2bfdfc5c0..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/format_es_buckets_for_histogram.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UMESBucket, UMESHistogramBucket } from '../adapters/database'; -import { dropLatestBucket } from './drop_latest_bucket'; - -/** - * The charting library we're currently using requires histogram data points have an - * x and an x0 property, where x0 is the beginning of a data point and x provides - * the size of the point from the start. This function attempts to generalize the - * concept so any bucket that has a numeric value as its key can be put into this format. - * - * Additionally, histograms that stack horizontally instead of vertically need to have - * a y and a y0 value. We're not doing this currently but with some minor modification - * this function could provide formatting for those buckets as well. - * @param buckets The ES data to format. - */ -export function formatEsBucketsForHistogram( - buckets: T[] -): Array { - // wait for first bucket to fill up - if (buckets.length < 2) { - return []; - } - const TERMINAL_INDEX = buckets.length - 1; - const { key: terminalBucketTime } = buckets[TERMINAL_INDEX]; - // drop the most recent bucket to avoid returning incomplete bucket - return dropLatestBucket(buckets).map((item, index, array) => { - const { key } = item; - const nextItem = array[index + 1]; - const bucketSize = nextItem ? Math.abs(nextItem.key - key) : Math.abs(terminalBucketTime - key); - - return { - x: key + bucketSize, - x0: key, - ...item, - }; - }); -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts index 107a635366a0b..0dedc3e456f51 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -11,7 +11,7 @@ export const getHistogramInterval = ( dateRangeStart: string, dateRangeEnd: string, bucketCount?: number -): string => { +): number => { const from = DateMath.parse(dateRangeStart); const to = DateMath.parse(dateRangeEnd); if (from === undefined) { @@ -20,7 +20,5 @@ export const getHistogramInterval = ( if (to === undefined) { throw Error('Invalid dateRangeEnd value'); } - return `${Math.round( - (to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT) - )}ms`; + return Math.round((to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT)); }; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts new file mode 100644 index 0000000000000..29af862611ca4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getHistogramInterval } from './get_histogram_interval'; + +export const getHistogramIntervalFormatted = ( + dateRangeStart: string, + dateRangeEnd: string, + bucketCount?: number +): string => `${getHistogramInterval(dateRangeStart, dateRangeEnd, bucketCount)}ms`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts index a2a72825c6b98..4c88da7eca85a 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { dropLatestBucket } from './drop_latest_bucket'; -export { formatEsBucketsForHistogram } from './format_es_buckets_for_histogram'; export { getFilterClause } from './get_filter_clause'; export { getHistogramInterval } from './get_histogram_interval'; +export { getHistogramIntervalFormatted } from './get_histogram_interval_formatted'; export { parseFilterQuery } from './parse_filter_query'; export { assertCloseTo } from './assert_close_to'; diff --git a/x-pack/package.json b/x-pack/package.json index fa439a2087547..089ce24b9a4ff 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -111,18 +111,13 @@ "@types/xml-crypto": "^1.4.0", "@types/xml2js": "^0.4.5", "abab": "^1.0.4", - "ansicolors": "0.3.2", "axios": "^0.19.0", "babel-jest": "^24.9.0", - "babel-plugin-inline-react-svg": "^1.1.0", - "babel-plugin-mock-imports": "^1.0.1", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "base64-js": "^1.3.1", "base64url": "^3.0.1", "chalk": "^2.4.2", "chance": "1.0.18", - "checksum": "0.1.1", "cheerio": "0.22.0", "commander": "3.0.0", "copy-webpack-plugin": "^5.0.4", @@ -143,7 +138,6 @@ "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", "gulp-mocha": "^7.0.2", - "gulp-multi-process": "1.3.1", "hapi": "^17.5.3", "jest": "^24.9.0", "jest-cli": "^24.9.0", @@ -155,7 +149,6 @@ "mocha-multi-reporters": "^1.1.7", "mochawesome": "^4.1.0", "mochawesome-merge": "^2.0.1", - "mochawesome-report-generator": "^4.0.1", "mustache": "^2.3.0", "mutation-observer": "^1.0.3", "node-fetch": "^2.6.0", @@ -168,8 +161,6 @@ "react-hooks-testing-library": "^0.3.8", "react-test-renderer": "^16.8.0", "react-testing-library": "^6.0.0", - "redux-test-utils": "0.2.2", - "rsync": "0.6.1", "sass-loader": "^7.3.1", "sass-resources-loader": "^2.0.1", "simple-git": "1.116.0", @@ -209,9 +200,7 @@ "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", "@mapbox/mapbox-gl-draw": "^1.1.1", - "@samverschueren/stream-to-observable": "^0.3.0", "@scant/router": "^0.1.0", - "@slack/client": "^4.8.0", "@slack/webhook": "^5.0.0", "@turf/boolean-contains": "6.0.1", "angular-resource": "1.7.8", @@ -242,7 +231,6 @@ "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", "d3-scale": "1.0.7", - "dataloader": "^1.4.0", "dedent": "^0.7.0", "del": "^5.1.0", "dragselect": "1.13.1", @@ -267,7 +255,6 @@ "handlebars": "4.3.5", "history": "4.9.0", "history-extra": "^5.0.1", - "humps": "2.0.1", "i18n-iso-countries": "^4.3.1", "icalendar": "0.7.1", "idx": "^2.5.6", @@ -310,7 +297,6 @@ "pluralize": "3.1.0", "pngjs": "3.4.0", "polished": "^1.9.2", - "popper.js": "^1.14.3", "postcss-prefix-selector": "^1.7.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", @@ -320,7 +306,6 @@ "react": "^16.8.0", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", - "react-clipboard.js": "^1.1.2", "react-datetime": "^2.14.0", "react-dom": "^16.8.0", "react-dropzone": "^4.2.9", @@ -330,11 +315,9 @@ "react-monaco-editor": "~0.27.0", "react-portal": "^3.2.0", "react-redux": "^5.1.1", - "react-redux-request": "^1.5.6", "react-resize-detector": "^4.2.0", "react-reverse-portal": "^1.0.4", "react-router-dom": "^4.3.1", - "react-select": "^1.2.1", "react-shortcuts": "^2.0.0", "react-sticky": "^6.0.3", "react-syntax-highlighter": "^5.7.0", diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts index 2e19f85875616..c2bc534f742a8 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts @@ -7,13 +7,13 @@ import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { spacesServiceMock } from '../../spaces_service/spaces_service.mock'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; const types = ['foo', 'bar', 'space']; const createMockRequest = () => ({}); -const createMockClient = () => SavedObjectsClientMock.create(); +const createMockClient = () => savedObjectsClientMock.create(); const createSpacesService = async (spaceId: string) => { return spacesServiceMock.createSetupContract(spaceId); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0d8d4d908231f..eabe830fa62f5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9318,7 +9318,6 @@ "xpack.siem.anomaliesTable.table.unit": "{totalCount, plural, =1 {anomaly} other {anomalies}}", "xpack.siem.clipboard.copy.to.the.clipboard": "クリップボードにコピー", "xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorButtonLabel": "インデックスパターンを編集", - "xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorDescription": "マップにイベントデータを表示するには、ECS に準拠した Kibana インデックスパターンを構成する必要があります。ビートを使用すると、次のセットアップコマンドを実行して必要な Kibana インデックスパターンを作成するか、Kibana の設定で手動により構成することができます。", "xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorTitle": "必要なインデックスパターンが構成されていません", "xpack.siem.components.ml.anomaly.errors.anomaliesTableFetchFailureTitle": "異常表の取得に失敗", "xpack.siem.components.ml.api.errors.networkErrorFailureTitle": "ネットワークエラー", @@ -10470,4 +10469,4 @@ "xpack.fileUpload.fileParser.errorReadingFile": "ファイルの読み込み中にエラーが発生しました", "xpack.fileUpload.fileParser.noFileProvided": "エラー、ファイルが提供されていません" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2e99d6d49a8e5..8d4a6f3773988 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9475,7 +9475,6 @@ "xpack.siem.anomaliesTable.table.unit": "{totalCount, plural, =1 {anomaly} other {anomalies}}", "xpack.siem.clipboard.copy.to.the.clipboard": "复制到剪贴板", "xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorButtonLabel": "配置索引模式", - "xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorDescription": "必须配置符合 ECS 的 Kibana 索引模式,才能查看地图上的数据。使用 Beats 时,您可以运行以下设置命令来创建所需的 Kibana 索引模式,否则只能在 Kibana 设置内手动配置。", "xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorTitle": "未配置所需的索引模式", "xpack.siem.components.ml.anomaly.errors.anomaliesTableFetchFailureTitle": "异常表提取失败", "xpack.siem.components.ml.api.errors.networkErrorFailureTitle": "网络错误:", @@ -10627,4 +10626,4 @@ "xpack.fileUpload.fileParser.errorReadingFile": "读取文件时出错", "xpack.fileUpload.fileParser.noFileProvided": "错误,未提供任何文件" } -} +} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_charts.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_charts.json index f5368ad7ecf0c..dbfc17a468796 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_charts.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_charts.json @@ -103,6 +103,10 @@ { "x": 1568173204510, "y": null + }, + { + "x": 1568173227311, + "y": 24627 } ] } @@ -257,6 +261,12 @@ "up": null, "down": null, "total": 0 + }, + { + "x": 1568173227311, + "up": null, + "down": null, + "total": 1 } ], "statusMaxCount": 0, diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram.json index c12ec7f3847c3..cf88ccae9cb99 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram.json @@ -1,179 +1,188 @@ { - "histogram": [ - { - "upCount": 93, - "downCount": 7, - "x": 1568172680087, - "x0": 1568172657286, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172702888, - "x0": 1568172680087, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172725689, - "x0": 1568172702888, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172748490, - "x0": 1568172725689, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172771291, - "x0": 1568172748490, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172794092, - "x0": 1568172771291, - "y": 1 - }, - { - "upCount": 92, - "downCount": 8, - "x": 1568172816893, - "x0": 1568172794092, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172839694, - "x0": 1568172816893, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172862495, - "x0": 1568172839694, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172885296, - "x0": 1568172862495, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172908097, - "x0": 1568172885296, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172930898, - "x0": 1568172908097, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172953699, - "x0": 1568172930898, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172976500, - "x0": 1568172953699, - "y": 1 - }, - { - "upCount": 92, - "downCount": 8, - "x": 1568172999301, - "x0": 1568172976500, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173022102, - "x0": 1568172999301, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173044903, - "x0": 1568173022102, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173067704, - "x0": 1568173044903, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173090505, - "x0": 1568173067704, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173113306, - "x0": 1568173090505, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173136107, - "x0": 1568173113306, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173158908, - "x0": 1568173136107, - "y": 1 - }, - { - "upCount": 92, - "downCount": 8, - "x": 1568173181709, - "x0": 1568173158908, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173204510, - "x0": 1568173181709, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173227311, - "x0": 1568173204510, - "y": 1 - } - ] + "queryResult": { + "histogram": [ + { + "upCount": 93, + "downCount": 7, + "x": 1568172657286, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172680087, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172702888, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172725689, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172748490, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172771291, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 8, + "x": 1568172794092, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172816893, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172839694, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172862495, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172885296, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172908097, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172930898, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172953699, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 8, + "x": 1568172976500, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172999301, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173022102, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173044903, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173067704, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568173090505, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173113306, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173136107, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 8, + "x": 1568173158908, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173181709, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568173204510, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173227311, + "x0": null, + "y": 1 + } + ] + } } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_filter.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_filter.json index f61a101ce4462..383d4acd96340 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_filter.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_filter.json @@ -1,179 +1,188 @@ { - "histogram": [ - { - "upCount": 93, - "downCount": 0, - "x": 1568172680087, - "x0": 1568172657286, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172702888, - "x0": 1568172680087, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172725689, - "x0": 1568172702888, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172748490, - "x0": 1568172725689, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172771291, - "x0": 1568172748490, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172794092, - "x0": 1568172771291, - "y": 1 - }, - { - "upCount": 92, - "downCount": 0, - "x": 1568172816893, - "x0": 1568172794092, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172839694, - "x0": 1568172816893, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172862495, - "x0": 1568172839694, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172885296, - "x0": 1568172862495, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172908097, - "x0": 1568172885296, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172930898, - "x0": 1568172908097, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172953699, - "x0": 1568172930898, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568172976500, - "x0": 1568172953699, - "y": 1 - }, - { - "upCount": 92, - "downCount": 0, - "x": 1568172999301, - "x0": 1568172976500, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173022102, - "x0": 1568172999301, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568173044903, - "x0": 1568173022102, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568173067704, - "x0": 1568173044903, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568173090505, - "x0": 1568173067704, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173113306, - "x0": 1568173090505, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568173136107, - "x0": 1568173113306, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568173158908, - "x0": 1568173136107, - "y": 1 - }, - { - "upCount": 92, - "downCount": 0, - "x": 1568173181709, - "x0": 1568173158908, - "y": 1 - }, - { - "upCount": 93, - "downCount": 0, - "x": 1568173204510, - "x0": 1568173181709, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173227311, - "x0": 1568173204510, - "y": 1 - } - ] + "queryResult": { + "histogram": [ + { + "upCount": 93, + "downCount": 0, + "x": 1568172657286, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172680087, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172702888, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172725689, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172748490, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172771291, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 0, + "x": 1568172794092, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172816893, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172839694, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172862495, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172885296, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172908097, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172930898, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568172953699, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 0, + "x": 1568172976500, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172999301, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173022102, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173044903, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173067704, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568173090505, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173113306, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173136107, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 0, + "x": 1568173158908, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173181709, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568173204510, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 0, + "x": 1568173227311, + "x0": null, + "y": 1 + } + ] + } } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_id.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_id.json index c12ec7f3847c3..cf88ccae9cb99 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_id.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_histogram_by_id.json @@ -1,179 +1,188 @@ { - "histogram": [ - { - "upCount": 93, - "downCount": 7, - "x": 1568172680087, - "x0": 1568172657286, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172702888, - "x0": 1568172680087, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172725689, - "x0": 1568172702888, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172748490, - "x0": 1568172725689, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172771291, - "x0": 1568172748490, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172794092, - "x0": 1568172771291, - "y": 1 - }, - { - "upCount": 92, - "downCount": 8, - "x": 1568172816893, - "x0": 1568172794092, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172839694, - "x0": 1568172816893, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172862495, - "x0": 1568172839694, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172885296, - "x0": 1568172862495, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172908097, - "x0": 1568172885296, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568172930898, - "x0": 1568172908097, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172953699, - "x0": 1568172930898, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568172976500, - "x0": 1568172953699, - "y": 1 - }, - { - "upCount": 92, - "downCount": 8, - "x": 1568172999301, - "x0": 1568172976500, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173022102, - "x0": 1568172999301, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173044903, - "x0": 1568173022102, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173067704, - "x0": 1568173044903, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173090505, - "x0": 1568173067704, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173113306, - "x0": 1568173090505, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173136107, - "x0": 1568173113306, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173158908, - "x0": 1568173136107, - "y": 1 - }, - { - "upCount": 92, - "downCount": 8, - "x": 1568173181709, - "x0": 1568173158908, - "y": 1 - }, - { - "upCount": 93, - "downCount": 7, - "x": 1568173204510, - "x0": 1568173181709, - "y": 1 - }, - { - "upCount": 0, - "downCount": 0, - "x": 1568173227311, - "x0": 1568173204510, - "y": 1 - } - ] + "queryResult": { + "histogram": [ + { + "upCount": 93, + "downCount": 7, + "x": 1568172657286, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172680087, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172702888, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172725689, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172748490, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172771291, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 8, + "x": 1568172794092, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172816893, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172839694, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172862495, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172885296, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172908097, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172930898, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568172953699, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 8, + "x": 1568172976500, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568172999301, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173022102, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173044903, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173067704, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568173090505, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173113306, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173136107, + "x0": null, + "y": 1 + }, + { + "upCount": 92, + "downCount": 8, + "x": 1568173158908, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173181709, + "x0": null, + "y": 1 + }, + { + "upCount": 0, + "downCount": 0, + "x": 1568173204510, + "x0": null, + "y": 1 + }, + { + "upCount": 93, + "downCount": 7, + "x": 1568173227311, + "x0": null, + "y": 1 + } + ] + } } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/snapshot_histogram.ts b/x-pack/test/api_integration/apis/uptime/graphql/snapshot_histogram.ts index 7af9de99d8327..02fd3fd630d4b 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/snapshot_histogram.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/snapshot_histogram.ts @@ -7,6 +7,7 @@ import { snapshotHistogramQueryString } from '../../../../../legacy/plugins/uptime/public/queries/snapshot_histogram_query'; import { expectFixtureEql } from './helpers/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { assertCloseTo } from '../../../../../legacy/plugins/uptime/server/lib/helper'; export default function({ getService }: FtrProviderContext) { describe('snapshotHistogram', () => { @@ -31,6 +32,10 @@ export default function({ getService }: FtrProviderContext) { .post('/api/uptime/graphql') .set('kbn-xsrf', 'foo') .send({ ...getSnapshotHistogramQuery }); + // manually testing this value and then removing it to avoid flakiness + const { interval } = data.queryResult; + assertCloseTo(interval, 22801, 100); + delete data.queryResult.interval; expectFixtureEql(data, 'snapshot_histogram'); }); @@ -50,6 +55,9 @@ export default function({ getService }: FtrProviderContext) { .post('/api/uptime/graphql') .set('kbn-xsrf', 'foo') .send({ ...getSnapshotHistogramQuery }); + const { interval } = data.queryResult; + assertCloseTo(interval, 22801, 100); + delete data.queryResult.interval; expectFixtureEql(data, 'snapshot_histogram_by_id'); }); @@ -71,6 +79,9 @@ export default function({ getService }: FtrProviderContext) { .post('/api/uptime/graphql') .set('kbn-xsrf', 'foo') .send({ ...getSnapshotHistogramQuery }); + const { interval } = data.queryResult; + assertCloseTo(interval, 22801, 100); + delete data.queryResult.interval; expectFixtureEql(data, 'snapshot_histogram_by_filter'); }); }); diff --git a/x-pack/test/functional/apps/logstash/pipeline_list.js b/x-pack/test/functional/apps/logstash/pipeline_list.js index d04f50690368d..ce0c9d881f51b 100644 --- a/x-pack/test/functional/apps/logstash/pipeline_list.js +++ b/x-pack/test/functional/apps/logstash/pipeline_list.js @@ -22,6 +22,9 @@ export default function ({ getService, getPageObjects }) { originalWindowSize = await browser.getWindowSize(); await browser.setWindowSize(1600, 1000); await esArchiver.load('logstash/example_pipelines'); + }); + + beforeEach(async () => { await PageObjects.logstash.gotoPipelineList(); }); @@ -86,10 +89,6 @@ export default function ({ getService, getPageObjects }) { await pipelineEditor.assertExists(); await pipelineEditor.assertDefaultInputs(); }); - - after(async () => { - await PageObjects.logstash.gotoPipelineList(); - }); }); describe('delete button', () => { @@ -122,15 +121,12 @@ export default function ({ getService, getPageObjects }) { describe('row links', () => { it('opens the selected row in the editor', async () => { + await PageObjects.logstash.gotoPipelineList(); await pipelineList.setFilter('tweets_and_beats'); await pipelineList.clickFirstRowId(); await pipelineEditor.assertExists(); await pipelineEditor.assertEditorId('tweets_and_beats'); }); - - after(async () => { - await PageObjects.logstash.gotoPipelineList(); - }); }); describe('next page button', () => { @@ -225,10 +221,6 @@ export default function ({ getService, getPageObjects }) { queueCheckpointWrites, }); }); - - after(async () => { - await PageObjects.logstash.gotoPipelineList(); - }); }); }); } diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index 06dd0df9e470c..6163e99b5eaa4 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -74,7 +74,7 @@ export default function({ getService }: FtrProviderContext) { describe('multi metric', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.load('ml/farequote'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index cd88a9bba1769..7ccd9214591f2 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -88,7 +88,7 @@ export default function({ getService }: FtrProviderContext) { describe('population', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.loadIfNeeded('ml/ecommerce'); + await esArchiver.load('ml/ecommerce'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index 2cca37a944563..5645bc7277d19 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -274,7 +274,7 @@ export default function({ getService }: FtrProviderContext) { describe('saved search', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.load('ml/farequote'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 6e640f7d173d5..06ec840b36aae 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -73,7 +73,7 @@ export default function({ getService }: FtrProviderContext) { describe('single metric', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.load('ml/farequote'); }); after(async () => { diff --git a/yarn.lock b/yarn.lock index b949aa07608c9..0c37018bdd596 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1064,10 +1064,10 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@elastic/charts@^13.5.7": - version "13.5.7" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-13.5.7.tgz#97ba458059613efd542ae68d7d2da059aac38484" - integrity sha512-8ibgrEJD3fpoLurB/DnNaWRmMGxAPHdtvCiPl1saPIjvlmGlrUNlXMneVgsPLqerNT0vuJDgqfQHHQcef/S2Hw== +"@elastic/charts@^13.5.9": + version "13.5.9" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-13.5.9.tgz#8e27ec7de934e20a9b853921cd453a372be99ef4" + integrity sha512-H5xsW/tEpjZhm0FpZThMyjuVBWlcXF2ImpfTWYv13p8GKmorCyQWbePau9Ya8N3lMmkHUMH2e95ifd3K3g9RgA== dependencies: "@types/d3-shape" "^1.3.1" classnames "^2.2.6" @@ -2418,35 +2418,6 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@slack/client@^4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@slack/client/-/client-4.8.0.tgz#265606f1cebae1d72f3fdd2cdf7cf1510783dde4" - integrity sha512-c4PKsRMtTp3QVYg+6cNqqxbU/50gnYfMlZgPCGUuMDMm9mkx50y0PEuERcVyLIe5j61imrhQx9DoNIfybEhTTw== - dependencies: - "@types/form-data" "^2.2.1" - "@types/is-stream" "^1.1.0" - "@types/loglevel" "^1.5.3" - "@types/node" ">=6.0.0" - "@types/p-cancelable" "^0.3.0" - "@types/p-queue" "^2.3.1" - "@types/p-retry" "^1.0.1" - "@types/retry" "^0.10.2" - "@types/ws" "^5.1.1" - axios "^0.18.0" - eventemitter3 "^3.0.0" - finity "^0.5.4" - form-data "^2.3.1" - is-stream "^1.1.0" - loglevel "^1.6.1" - object.entries "^1.0.4" - object.getownpropertydescriptors "^2.0.3" - object.values "^1.0.4" - p-cancelable "^0.3.0" - p-queue "^2.3.0" - p-retry "^2.0.0" - retry "^0.12.0" - ws "^5.2.0" - "@slack/types@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.0.0.tgz#1dc7a63b293c4911e474197585c3feda012df17a" @@ -3308,13 +3279,6 @@ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.0.tgz#cbb49815a5e1129d5f23836a98d65d93822409af" integrity sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ== -"@types/form-data@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" - integrity sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ== - dependencies: - "@types/node" "*" - "@types/fs-extra@5.0.4": version "5.0.4" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.4.tgz#b971134d162cc0497d221adde3dbb67502225599" @@ -3451,11 +3415,6 @@ resolved "https://registry.yarnpkg.com/@types/hoek/-/hoek-4.1.3.tgz#d1982d48fb0d2a0e5d7e9d91838264d8e428d337" integrity sha1-0ZgtSPsNKg5dfp2Rg4Jk2OQo0zc= -"@types/humps@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/humps/-/humps-1.1.2.tgz#fbcaf596d20ff2ed78f8f511c5d6a943b51101d6" - integrity sha1-+8r1ltIP8u14+PURxdapQ7URAdY= - "@types/indent-string@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" @@ -3478,13 +3437,6 @@ resolved "https://registry.yarnpkg.com/@types/is-glob/-/is-glob-4.0.0.tgz#fb8a2bff539025d4dcd6d5efe7689e03341b876d" integrity sha512-zC/2EmD8scdsGIeE+Xg7kP7oi9VP90zgMQtm9Cr25av4V+a+k8slQyiT60qSw8KORYrOKlPXfHwoa1bQbRzskQ== -"@types/is-stream@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" - integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg== - dependencies: - "@types/node" "*" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -3622,11 +3574,6 @@ resolved "https://registry.yarnpkg.com/@types/log-symbols/-/log-symbols-2.0.0.tgz#7919e2ec3c8d13879bfdcab310dd7a3f7fc9466d" integrity sha512-YJhbp0sz3egFFKl3BcCNPQKzuGFOP4PACcsifhK6ROGnJUW9ViYLuLybQ9GQZm7Zejy3tkGuiXYMq3GiyGkU4g== -"@types/loglevel@^1.5.3": - version "1.5.3" - resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8" - integrity sha512-TzzIZihV+y9kxSg5xJMkyIkaoGkXi50isZTtGHObNHRqAAwjGNjSCNPI7AUAv0tZUKTq9f2cdkCUd/2JVZUTrA== - "@types/lru-cache@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" @@ -3723,7 +3670,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@10.12.27", "@types/node@8.5.8", "@types/node@>=6.0.0", "@types/node@>=8.9.0", "@types/node@^10.12.27", "@types/node@^12.0.2": +"@types/node@*", "@types/node@10.12.27", "@types/node@8.5.8", "@types/node@>=8.9.0", "@types/node@^10.12.27", "@types/node@^12.0.2": version "10.12.27" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.27.tgz#eb3843f15d0ba0986cc7e4d734d2ee8b50709ef8" integrity sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg== @@ -3766,23 +3713,6 @@ dependencies: "@types/node" "*" -"@types/p-cancelable@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-0.3.0.tgz#3e4fcc54a3dfd81d0f5b93546bb68d0df50553bb" - integrity sha512-sP+9Ivnpil7cdmvr5O+145aXm65YX8Y+Lrul1ojdYz6yaE05Dqonn6Z9v5eqJCQ0UeSGcTRtepMlZDh9ywdKgw== - -"@types/p-queue@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.1.tgz#2fb251e46e884e31c4bd1bf58f0e188972353ff4" - integrity sha512-JyO7uMAtkcMMULmsTQ4t/lCC8nxirTtweGG1xAFNNIAoC1RemmeIxq8PiKghuEy99XdbS6Lwx4zpbXUjfeSSAA== - -"@types/p-retry@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/p-retry/-/p-retry-1.0.1.tgz#2302bc3da425014208c8a9b68293d37325124785" - integrity sha512-HgQPG9kkUb4EpTeUv2taH2nBZsVUb5aOTSw3X2YozcTG1ttmGcLaLKx1MbAz1evVfUEDTCAPmdz2HiFztIyWrw== - dependencies: - "@types/retry" "*" - "@types/papaparse@^4.5.11": version "4.5.11" resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.5.11.tgz#dcd4f64da55f768c2e2cf92ccac1973c67a73890" @@ -3982,11 +3912,6 @@ "@types/tough-cookie" "*" form-data "^2.5.0" -"@types/retry@*", "@types/retry@^0.10.2": - version "0.10.2" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" - integrity sha512-LqJkY4VQ7S09XhI7kA3ON71AxauROhSv74639VsNXC9ish4IWHnIi98if+nP1MxQV3RMPqXSCYgpPsDHjlg9UQ== - "@types/selenium-webdriver@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.3.tgz#388f12c464cc1fff5d4c84cb372f19b9ab9b5c81" @@ -4207,14 +4132,6 @@ resolved "https://registry.yarnpkg.com/@types/write-pkg/-/write-pkg-3.1.0.tgz#f58767f4fb9a6a3ad8e95d3e9cd1f2d026ceab26" integrity sha512-JRGsPEPCrYqTXU0Cr+Yu7esPBE2yvH7ucOHr+JuBy0F59kglPvO5gkmtyEvf3P6dASSkScvy/XQ6SC1QEBFDuA== -"@types/ws@^5.1.1": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-5.1.2.tgz#f02d3b1cd46db7686734f3ce83bdf46c49decd64" - integrity sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg== - dependencies: - "@types/events" "*" - "@types/node" "*" - "@types/xml-crypto@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@types/xml-crypto/-/xml-crypto-1.4.0.tgz#b586e4819f6bdd0571a3faa9a8098049d5c3cc5a" @@ -5082,16 +4999,16 @@ ansi@^0.3.0, ansi@~0.3.1: resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" integrity sha1-DELU+xcWDVqa8eSEus4cZpIsGyE= -ansicolors@0.3.2, ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= - ansicolors@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" integrity sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8= +ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= + any-base@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe" @@ -5670,16 +5587,6 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -assignment@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.0.0.tgz#ffd17b21bf5d6b22e777b989681a815456a3dd3e" - integrity sha1-/9F7Ib9dayLnd7mJaBqBVFaj3T4= - -assignment@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.2.0.tgz#f5b5bc2d160d69986e8700cd38f567c0aabe101e" - integrity sha1-9bW8LRYNaZhuhwDNOPVnwKq+EB4= - ast-module-types@^2.3.1, ast-module-types@^2.3.2, ast-module-types@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-2.5.0.tgz#44b8bcd51684329a77f2af6b2587df9ea6b4d5ff" @@ -5763,55 +5670,6 @@ async-settle@^1.0.0: dependencies: async-done "^1.2.2" -async.queue@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.queue/-/async.queue-0.5.2.tgz#8d5d90812e1481066bc0904e8cc1712b17c3bd7c" - integrity sha1-jV2QgS4UgQZrwJBOjMFxKxfDvXw= - dependencies: - async.util.queue "0.5.2" - -async.util.arrayeach@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.arrayeach/-/async.util.arrayeach-0.5.2.tgz#58c4e98028d55d69bfb05aeb3af44e0a555a829c" - integrity sha1-WMTpgCjVXWm/sFrrOvROClVagpw= - -async.util.isarray@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.isarray/-/async.util.isarray-0.5.2.tgz#e62dac8f2636f65875dcf7521c2d24d0dfb2bbdf" - integrity sha1-5i2sjyY29lh13PdSHC0k0N+yu98= - -async.util.map@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.map/-/async.util.map-0.5.2.tgz#e588ef86e0b3ab5f027d97af4d6835d055ca69d6" - integrity sha1-5YjvhuCzq18CfZevTWg10FXKadY= - -async.util.noop@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.noop/-/async.util.noop-0.5.2.tgz#bdd62b97cb0aa3f60b586ad148468698975e58b9" - integrity sha1-vdYrl8sKo/YLWGrRSEaGmJdeWLk= - -async.util.onlyonce@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.onlyonce/-/async.util.onlyonce-0.5.2.tgz#b8e6fc004adc923164d79e32f2813ee465c24ff2" - integrity sha1-uOb8AErckjFk154y8oE+5GXCT/I= - -async.util.queue@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.queue/-/async.util.queue-0.5.2.tgz#57f65abe1a3cdf273d31abd28ab95425f8222ee5" - integrity sha1-V/Zavho83yc9MavSirlUJfgiLuU= - dependencies: - async.util.arrayeach "0.5.2" - async.util.isarray "0.5.2" - async.util.map "0.5.2" - async.util.noop "0.5.2" - async.util.onlyonce "0.5.2" - async.util.setimmediate "0.5.2" - -async.util.setimmediate@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/async.util.setimmediate/-/async.util.setimmediate-0.5.2.tgz#2812ebabf2a58027758d4bc7793d1ccfaf10255f" - integrity sha1-KBLrq/KlgCd1jUvHeT0cz68QJV8= - async@1.x, async@^1.4.2, async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -6112,17 +5970,6 @@ babel-plugin-emotion@^9.2.11: source-map "^0.5.7" touch "^2.0.1" -babel-plugin-inline-react-svg@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-inline-react-svg/-/babel-plugin-inline-react-svg-1.1.0.tgz#b39519c78249b3fcf895b541c38b485a2b11b0be" - integrity sha512-Y/tBMi7Jh7Jh+DGcSNsY9/RW33nvcR067HFK0Dp+03jpidil1sJAffBdajK72xn3tbwMsgFLJubxW5xpQLJytA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/parser" "^7.0.0" - lodash.isplainobject "^4.0.6" - resolve "^1.10.0" - svgo "^0.7.2" - babel-plugin-istanbul@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.1.tgz#7981590f1956d75d67630ba46f0c22493588c893" @@ -6240,11 +6087,6 @@ babel-plugin-minify-type-constructors@^0.4.3: dependencies: babel-helper-is-void-0 "^0.4.3" -babel-plugin-mock-imports@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-plugin-mock-imports/-/babel-plugin-mock-imports-1.0.1.tgz#1476ed4de911347d344fc81caab4beced80804b1" - integrity sha512-Nu4unCGKeqOfLlfnLPnv/pEHancdAGTqFqyArZ27gsKIiKxeZvMr87IHB8BxhMu3Bfc8fA8bx7hWt32aZbEwpQ== - babel-plugin-named-asset-import@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.3.tgz#9ba2f3ac4dc78b042651654f07e847adfe50667c" @@ -6304,7 +6146,7 @@ babel-plugin-transform-property-literals@^6.9.4: dependencies: esutils "^2.0.2" -babel-plugin-transform-react-remove-prop-types@0.4.24, babel-plugin-transform-react-remove-prop-types@^0.4.24: +babel-plugin-transform-react-remove-prop-types@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== @@ -7747,13 +7589,6 @@ check-more-types@2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= -checksum@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/checksum/-/checksum-0.1.1.tgz#dc6527d4c90be8560dbd1ed4cecf3297d528e9e9" - integrity sha1-3GUn1MkL6FYNvR7Uzs8yl9Uo6ek= - dependencies: - optimist "~0.3.5" - cheerio@0.22.0: version "0.22.0" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" @@ -7907,13 +7742,6 @@ circular-json@^0.5.5: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== -clap@^1.0.9: - version "1.2.3" - resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" - integrity sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA== - dependencies: - chalk "^1.1.3" - class-extend@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/class-extend/-/class-extend-0.1.2.tgz#8057a82b00f53f82a5d62c50ef8cffdec6fabc34" @@ -7937,7 +7765,7 @@ classnames@2.2.6, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -classnames@2.x, classnames@^2.2.4: +classnames@2.x: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" integrity sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0= @@ -8067,15 +7895,6 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= -clipboard@^1.6.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b" - integrity sha1-Ng1taUbpmnof7zleQrqStem1oWs= - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - clipboard@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d" @@ -8217,13 +8036,6 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" -coa@~1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" - integrity sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0= - dependencies: - q "^1.1.2" - code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -9275,14 +9087,6 @@ csso@^3.5.1: dependencies: css-tree "1.0.0-alpha.29" -csso@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" - integrity sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U= - dependencies: - clap "^1.0.9" - source-map "^0.5.3" - cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" @@ -9696,11 +9500,6 @@ data-urls@^1.0.1: whatwg-mimetype "^2.1.0" whatwg-url "^7.0.0" -dataloader@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" - integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== - date-fns@^1.27.2: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" @@ -12680,11 +12479,6 @@ fined@^1.0.1: object.pick "^1.2.0" parse-filepath "^1.0.1" -finity@^0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/finity/-/finity-0.5.4.tgz#f2a8a9198e8286467328ec32c8bfcc19a2229c11" - integrity sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA== - first-chunk-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70" @@ -14372,13 +14166,6 @@ gulp-mocha@^7.0.2: supports-color "^7.0.0" through2 "^3.0.1" -gulp-multi-process@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/gulp-multi-process/-/gulp-multi-process-1.3.1.tgz#e12aa818e4c234357ad99d5caff8df8a18f46e9e" - integrity sha512-okxYy3mxUkekM0RNjkBg8OPuzpnD2yXMAdnGOaQPSJ2wzBdE9R9pkTV+tzPZ65ORK7b57YUc6s+gROA4+EIOLg== - dependencies: - async.queue "^0.5.2" - gulp-rename@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd" @@ -14758,11 +14545,6 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" - integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI= - he@1.2.0, he@1.2.x, he@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -15155,11 +14937,6 @@ humanize-string@^1.0.2: dependencies: decamelize "^1.0.0" -humps@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" - integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao= - hyperlinker@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" @@ -15638,14 +15415,6 @@ inquirer@^7.0.0: strip-ansi "^5.1.0" through "^2.3.6" -insane@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/insane/-/insane-2.6.1.tgz#c7dcae7b51c20346883b71078fad6ce0483c198f" - integrity sha1-x9yue1HCA0aIO3EHj61s4Eg8GY8= - dependencies: - assignment "2.0.0" - he "0.5.0" - insight@0.10.1: version "0.10.1" resolved "https://registry.yarnpkg.com/insight/-/insight-0.10.1.tgz#a0ecf668484a95d66e9be59644964e719cc83380" @@ -17161,7 +16930,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1, js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0, js-yaml@~3.13.0, js-yaml@~3.13.1, js-yaml@~3.7.0: +js-yaml@3.13.1, js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0, js-yaml@~3.13.0, js-yaml@~3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -18577,11 +18346,6 @@ logform@^2.1.1: ms "^2.1.1" triple-beam "^1.3.0" -loglevel@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" - integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= - loglevel@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.4.tgz#f408f4f006db8354d0577dcf6d33485b3cb90d56" @@ -19566,7 +19330,7 @@ mochawesome-merge@^2.0.1: uuid "^3.3.2" yargs "^12.0.5" -mochawesome-report-generator@^4.0.0, mochawesome-report-generator@^4.0.1: +mochawesome-report-generator@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/mochawesome-report-generator/-/mochawesome-report-generator-4.0.1.tgz#0a010d1ecf379eb26ba05300feb59e2665076080" integrity sha512-hQbmQt8/yCT68GjrQFat+Diqeuka3haNllexYfja1+y0hpwi3yCJwFpQCdWK9ezzcXL3Nu80f2I6SZeyspwsqg== @@ -20819,13 +20583,6 @@ optimist@^0.6.1, optimist@~0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" -optimist@~0.3.5: - version "0.3.7" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" - integrity sha1-yQlBrVnkJzMokjB00s8ufLxuwNk= - dependencies: - wordwrap "~0.0.2" - optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" @@ -21087,23 +20844,11 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" -p-queue@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-2.4.2.tgz#03609826682b743be9a22dba25051bd46724fc34" - integrity sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng== - p-reduce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= -p-retry@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-2.0.0.tgz#b97f1f4d6d81a3c065b2b40107b811e995c1bfba" - integrity sha512-ZbCuzAmiwJ45q4evp/IG9D+5MUllGSUeCWwPt3j/tdYSi1KPkSD+46uqmAA1LhccDhOXv8kYZKNb8x78VflzfA== - dependencies: - retry "^0.12.0" - p-retry@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -21868,7 +21613,7 @@ polished@^3.3.1: dependencies: "@babel/runtime" "^7.4.5" -popper.js@^1.14.1, popper.js@^1.14.3, popper.js@^1.14.7: +popper.js@^1.14.1, popper.js@^1.14.7: version "1.15.0" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== @@ -22208,7 +21953,7 @@ prop-types@15.6.1: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -22838,14 +22583,6 @@ react-clientside-effect@^1.2.0: "@babel/runtime" "^7.0.0" shallowequal "^1.1.0" -react-clipboard.js@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-1.1.3.tgz#86feeb49364553ecd15aea91c75aa142532a60e0" - integrity sha512-97IKPinjiuFIBrCXqhNvKCBJFrSS1mmV5LVALE9djkweau26UWpR5VueYB3Eo3b2vfPtbyt0QUw06YOGdC0rpw== - dependencies: - clipboard "^1.6.1" - prop-types "^15.5.0" - react-color@^2.13.8: version "2.14.1" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.14.1.tgz#db8ad4f45d81e74896fc2e1c99508927c6d084e0" @@ -23073,7 +22810,7 @@ react-hotkeys@2.0.0-pre4: dependencies: prop-types "^15.6.1" -react-input-autosize@^2.1.2, react-input-autosize@^2.2.1: +react-input-autosize@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" integrity sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA== @@ -23265,15 +23002,6 @@ react-reconciler@^0.20.1: prop-types "^15.6.2" scheduler "^0.13.6" -react-redux-request@^1.5.6: - version "1.5.6" - resolved "https://registry.yarnpkg.com/react-redux-request/-/react-redux-request-1.5.6.tgz#8c514dc88264d225e113b4b54a265064e8020651" - integrity sha512-mzdG41GSLwynFI7DII3XNJxkABLD++I3Q1zlZWpcqycWSzWSYkjPUEz7M8r6aIIMzruANHQZX+asulvoaiwFRg== - dependencies: - lodash.get "^4.4.2" - lodash.isequal "^4.5.0" - prop-types "^15.6.1" - react-redux@^5.0.6, react-redux@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" @@ -23391,15 +23119,6 @@ react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" -react-select@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.1.tgz#a2fe58a569eb14dcaa6543816260b97e538120d1" - integrity sha512-vaCgT2bEl+uTyE/uKOEgzE5Dc/wLtzhnBvoHCeuLoJWc4WuadN6WQDhoL42DW+TziniZK2Gaqe/wUXydI3NSaQ== - dependencies: - classnames "^2.2.4" - prop-types "^15.5.8" - react-input-autosize "^2.1.2" - react-select@^2.2.0: version "2.4.4" resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73" @@ -23934,11 +23653,6 @@ redux-saga@^0.16.0: resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.16.2.tgz#993662e86bc945d8509ac2b8daba3a8c615cc971" integrity sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w== -redux-test-utils@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/redux-test-utils/-/redux-test-utils-0.2.2.tgz#593213f30173c5908f72315f08b705e1606094fe" - integrity sha512-+YsUHpzZJ7G85wYgllmGLJ75opIlWrCuKThaVTsHW5xLOrzaLE4abQ3AbYcHkx/vFOReG2D8XUwMfGnFKH8hGw== - redux-thunk@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" @@ -24816,11 +24530,6 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -rsync@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/rsync/-/rsync-0.6.1.tgz#3681a0098bd8750448f8bf9da1fee09f7763742b" - integrity sha1-NoGgCYvYdQRI+L+dof7gn3djdCs= - run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -25056,7 +24765,7 @@ sass-resources-loader@^2.0.1: glob "^7.1.1" loader-utils "^1.0.4" -sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.1, sax@~1.2.4: +sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -26257,11 +25966,6 @@ stream-spigot@~2.1.2: dependencies: readable-stream "~1.1.0" -stream-stream@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/stream-stream/-/stream-stream-1.2.6.tgz#a9ae071c64c11b8584f52973f7715e37e5144c43" - integrity sha1-qa4HHGTBG4WE9Slz93FeN+UUTEM= - stream-to-observable@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" @@ -26770,19 +26474,6 @@ svg-to-pdfkit@^0.1.7: dependencies: pdfkit ">=0.8.1" -svgo@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" - integrity sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U= - dependencies: - coa "~1.0.1" - colors "~1.1.2" - csso "~2.3.1" - js-yaml "~3.7.0" - mkdirp "~0.5.1" - sax "~1.2.1" - whet.extend "~0.9.9" - svgo@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.2.tgz#0253d34eccf2aed4ad4f283e11ee75198f9d7316" @@ -27606,25 +27297,6 @@ trough@^1.0.0: dependencies: glob "^6.0.4" -trunc-html@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/trunc-html/-/trunc-html-1.1.2.tgz#1e97d51f67d470b67662b1a670e6d0ea7a8edafe" - integrity sha1-HpfVH2fUcLZ2YrGmcObQ6nqO2v4= - dependencies: - assignment "2.2.0" - insane "2.6.1" - trunc-text "1.0.1" - -trunc-text@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5" - integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU= - -trunc-text@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.2.tgz#b582bb3ddea9c9adc25017d737c48ebdd2157406" - integrity sha1-tYK7Pd6pya3CUBfXN8SOvdIVdAY= - ts-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ts-debounce/-/ts-debounce-1.0.0.tgz#e433301744ba75fe25466f7f23e1382c646aae6a" @@ -29954,11 +29626,6 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -whet.extend@~0.9.9: - version "0.9.9" - resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" - integrity sha1-+HfVv2SMl+WqVC+twW1qJZucEaE= - which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" @@ -30254,13 +29921,6 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" - integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== - dependencies: - async-limiter "~1.0.0" - ws@^6.1.0: version "6.2.0" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.0.tgz#13806d9913b2a5f3cbb9ba47b563c002cbc7c526"