From 175bdb588fa3181e488d111a7d4954c3913a3d87 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 30 Jun 2020 17:00:45 -0600 Subject: [PATCH 01/29] [QA] [Code Coverage] Integrate with Team Assignment Pipeline and Add Research and Development Indexes and Cluster (#69348) Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_coverage | 2 +- .github/CODEOWNERS | 1 + .../ingest_coverage/__tests__/ingest.test.js | 37 --------- .../__tests__/ingest_helpers.test.js | 75 +++++++++++++++++ .../__tests__/transforms.test.js | 38 ++++++--- .../ingest_coverage/constants.js | 13 +++ .../code_coverage/ingest_coverage/ingest.js | 81 ++++++++++++++----- .../ingest_coverage/ingest_helpers.js | 28 +++++++ .../integration_tests/ingest_coverage.test.js | 47 +++++------ .../coverage-summary-NO-total.json | 0 .../code_coverage/ingest_coverage/process.js | 6 +- .../shell_scripts/ingest_coverage.sh | 10 ++- vars/kibanaCoverage.groovy | 12 +-- 13 files changed, 244 insertions(+), 106 deletions(-) delete mode 100644 src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js create mode 100644 src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js delete mode 100644 src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-NO-total.json diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 650ef94e1d3da..d6600256bab7b 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -28,7 +28,7 @@ def handleIngestion(timestamp) { kibanaCoverage.collectVcsInfo("### Collect VCS Info") kibanaCoverage.generateReports("### Merge coverage reports") kibanaCoverage.uploadCombinedReports() - kibanaCoverage.ingest(timestamp, '### Injest && Upload') + kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, '### Ingest && Upload') kibanaCoverage.uploadCoverageStaticSite(timestamp) } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47f9942162f75..a94180e60e05e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -132,6 +132,7 @@ # Quality Assurance /src/dev/code_coverage @elastic/kibana-qa +/vars/*Coverage.groovy @elastic/kibana-qa /test/functional/services/common @elastic/kibana-qa /test/functional/services/lib @elastic/kibana-qa /test/functional/services/remote @elastic/kibana-qa diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js deleted file mode 100644 index ad5b4da0873b9..0000000000000 --- a/src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { maybeTeamAssign } from '../ingest'; -import { COVERAGE_INDEX, TOTALS_INDEX } from '../constants'; - -describe(`Ingest fns`, () => { - describe(`maybeTeamAssign fn`, () => { - describe(`against the coverage index`, () => { - it(`should have the pipeline prop`, () => { - expect(maybeTeamAssign(COVERAGE_INDEX, {})).to.have.property('pipeline'); - }); - }); - describe(`against the totals index`, () => { - it(`should not have the pipeline prop`, () => { - expect(maybeTeamAssign(TOTALS_INDEX, {})).not.to.have.property('pipeline'); - }); - }); - }); -}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js new file mode 100644 index 0000000000000..7ca7279e0d64c --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { maybeTeamAssign, whichIndex } from '../ingest_helpers'; +import { + TOTALS_INDEX, + RESEARCH_TOTALS_INDEX, + RESEARCH_COVERAGE_INDEX, + // COVERAGE_INDEX, +} from '../constants'; + +describe(`Ingest Helper fns`, () => { + describe(`whichIndex`, () => { + describe(`against the research job`, () => { + const whichIndexAgainstResearchJob = whichIndex(true); + describe(`against the totals index`, () => { + const isTotal = true; + it(`should return the Research Totals Index`, () => { + const actual = whichIndexAgainstResearchJob(isTotal); + expect(actual).to.be(RESEARCH_TOTALS_INDEX); + }); + }); + describe(`against the coverage index`, () => { + it(`should return the Research Totals Index`, () => { + const isTotal = false; + const actual = whichIndexAgainstResearchJob(isTotal); + expect(actual).to.be(RESEARCH_COVERAGE_INDEX); + }); + }); + }); + describe(`against the "prod" job`, () => { + const whichIndexAgainstProdJob = whichIndex(false); + describe(`against the totals index`, () => { + const isTotal = true; + it(`should return the "Prod" Totals Index`, () => { + const actual = whichIndexAgainstProdJob(isTotal); + expect(actual).to.be(TOTALS_INDEX); + }); + }); + }); + }); + describe(`maybeTeamAssign`, () => { + describe(`against a coverage index`, () => { + it(`should have the pipeline prop`, () => { + const actual = maybeTeamAssign(true, { a: 'blah' }); + expect(actual).to.have.property('pipeline'); + }); + }); + describe(`against a totals index`, () => { + describe(`for "prod"`, () => { + it(`should not have the pipeline prop`, () => { + const actual = maybeTeamAssign(false, { b: 'blah' }); + expect(actual).not.to.have.property('pipeline'); + }); + }); + }); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 8c982b792ed3b..2fd1d5cbe8d48 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -32,17 +32,33 @@ describe(`Transform fn`, () => { }); }); describe(`coveredFilePath`, () => { - it(`should remove the jenkins workspace path`, () => { - const obj = { - staticSiteUrl: - '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', - COVERAGE_INGESTION_KIBANA_ROOT: - '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', - }; - expect(coveredFilePath(obj)).to.have.property( - 'coveredFilePath', - 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' - ); + describe(`in the code-coverage job`, () => { + it(`should remove the jenkins workspace path`, () => { + const obj = { + staticSiteUrl: + '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', + COVERAGE_INGESTION_KIBANA_ROOT: + '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', + }; + expect(coveredFilePath(obj)).to.have.property( + 'coveredFilePath', + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' + ); + }); + }); + describe(`in the qa research job`, () => { + it(`should remove the jenkins workspace path`, () => { + const obj = { + staticSiteUrl: + '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', + COVERAGE_INGESTION_KIBANA_ROOT: + '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana', + }; + expect(coveredFilePath(obj)).to.have.property( + 'coveredFilePath', + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' + ); + }); }); }); describe(`itemizeVcs`, () => { diff --git a/src/dev/code_coverage/ingest_coverage/constants.js b/src/dev/code_coverage/ingest_coverage/constants.js index a7303f0778d1c..ddee7106f4490 100644 --- a/src/dev/code_coverage/ingest_coverage/constants.js +++ b/src/dev/code_coverage/ingest_coverage/constants.js @@ -18,4 +18,17 @@ */ export const COVERAGE_INDEX = process.env.COVERAGE_INDEX || 'kibana_code_coverage'; + export const TOTALS_INDEX = process.env.TOTALS_INDEX || `kibana_total_code_coverage`; + +export const RESEARCH_COVERAGE_INDEX = + process.env.RESEARCH_COVERAGE_INDEX || 'qa_research_code_coverage'; + +export const RESEARCH_TOTALS_INDEX = + process.env.RESEARCH_TOTALS_INDEX || `qa_research_total_code_coverage`; + +export const TEAM_ASSIGNMENT_PIPELINE_NAME = process.env.PIPELINE_NAME || 'team_assignment'; + +export const CODE_COVERAGE_CI_JOB_NAME = 'elastic+kibana+code-coverage'; +export const RESEARCH_CI_JOB_NAME = 'elastic+kibana+qa-research'; +export const CI_JOB_NAME = process.env.COVERAGE_JOB_NAME || RESEARCH_CI_JOB_NAME; diff --git a/src/dev/code_coverage/ingest_coverage/ingest.js b/src/dev/code_coverage/ingest_coverage/ingest.js index d6c55a9a655b8..43f0663ad0359 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest.js +++ b/src/dev/code_coverage/ingest_coverage/ingest.js @@ -19,40 +19,77 @@ const { Client } = require('@elastic/elasticsearch'); import { createFailError } from '@kbn/dev-utils'; -import { COVERAGE_INDEX, TOTALS_INDEX } from './constants'; -import { errMsg, redact } from './ingest_helpers'; -import { noop } from './utils'; +import { RESEARCH_CI_JOB_NAME, TEAM_ASSIGNMENT_PIPELINE_NAME } from './constants'; +import { errMsg, redact, whichIndex } from './ingest_helpers'; +import { pretty, green } from './utils'; import { right, left } from './either'; const node = process.env.ES_HOST || 'http://localhost:9200'; + const client = new Client({ node }); -const pipeline = process.env.PIPELINE_NAME || 'team_assignment'; -const redacted = redact(node); +const redactedEsHostUrl = redact(node); +const parse = JSON.parse.bind(null); +const isResearchJob = process.env.COVERAGE_JOB_NAME === RESEARCH_CI_JOB_NAME ? true : false; export const ingest = (log) => async (body) => { - const index = body.isTotal ? TOTALS_INDEX : COVERAGE_INDEX; - const maybeWithPipeline = maybeTeamAssign(index, body); - const withIndex = { index, body: maybeWithPipeline }; - const dontSend = noop; - - log.verbose(withIndex); - - process.env.NODE_ENV === 'integration_test' - ? left(null) - : right(withIndex).fold(dontSend, async function doSend(finalPayload) { - await send(index, redacted, finalPayload); - }); + const isTotal = !!body.isTotal; + const index = whichIndex(isResearchJob)(isTotal); + const isACoverageIndex = isTotal ? false : true; + + const stringified = pretty(body); + const pipeline = TEAM_ASSIGNMENT_PIPELINE_NAME; + + const finalPayload = isACoverageIndex + ? { index, body: stringified, pipeline } + : { index, body: stringified }; + + const justLog = dontSendButLog(log); + const doSendToIndex = doSend(index); + const doSendRedacted = doSendToIndex(redactedEsHostUrl)(log)(client); + + eitherSendOrNot(finalPayload).fold(justLog, doSendRedacted); }; -async function send(idx, redacted, requestBody) { +function doSend(index) { + return (redactedEsHostUrl) => (log) => (client) => async (payload) => { + const logF = logSend(true)(redactedEsHostUrl)(log); + await send(logF, index, redactedEsHostUrl, client, payload); + }; +} + +function dontSendButLog(log) { + return (payload) => { + logSend(false)(null)(log)(payload); + }; +} + +async function send(logF, idx, redactedEsHostUrl, client, requestBody) { try { await client.index(requestBody); + logF(requestBody); } catch (e) { - throw createFailError(errMsg(idx, redacted, requestBody, e)); + const { body } = requestBody; + const parsed = parse(body); + throw createFailError(errMsg(idx, redactedEsHostUrl, parsed, e)); } } -export function maybeTeamAssign(index, body) { - const payload = index === TOTALS_INDEX ? body : { ...body, pipeline }; - return payload; +const sendMsg = (actuallySent, redactedEsHostUrl, payload) => { + const { index, body } = payload; + return `### ${actuallySent ? 'Sent' : 'Fake Sent'}: +${redactedEsHostUrl ? `\t### ES Host: ${redactedEsHostUrl}` : ''} +\t### Index: ${green(index)} +\t### payload.body: ${body} +${process.env.NODE_ENV === 'integration_test' ? `ingest-pipe=>${payload.pipeline}` : ''} +`; +}; + +function logSend(actuallySent) { + return (redactedEsHostUrl) => (log) => (payload) => { + log.verbose(sendMsg(actuallySent, redactedEsHostUrl, payload)); + }; +} + +function eitherSendOrNot(payload) { + return process.env.NODE_ENV === 'integration_test' ? left(payload) : right(payload); } diff --git a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js index 11e5755bb0282..86bcf03977082 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js +++ b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js @@ -20,6 +20,13 @@ import { always, pretty } from './utils'; import chalk from 'chalk'; import { fromNullable } from './either'; +import { + COVERAGE_INDEX, + RESEARCH_COVERAGE_INDEX, + RESEARCH_TOTALS_INDEX, + TEAM_ASSIGNMENT_PIPELINE_NAME, + TOTALS_INDEX, +} from './constants'; export function errMsg(index, redacted, body, e) { const orig = fromNullable(e.body).fold( @@ -38,6 +45,9 @@ ${orig} ### Troubleshooting Hint: ${red('Perhaps the coverage data was not merged properly?\n')} + +### Error.meta (stringified): +${pretty(e.meta)} `; } @@ -59,3 +69,21 @@ function color(whichColor) { return chalk[whichColor].bgWhiteBright(x); }; } + +export function maybeTeamAssign(isACoverageIndex, body) { + const doAddTeam = isACoverageIndex ? true : false; + const payload = doAddTeam ? { ...body, pipeline: TEAM_ASSIGNMENT_PIPELINE_NAME } : body; + return payload; +} + +export function whichIndex(isResearchJob) { + return (isTotal) => + isTotal ? whichTotalsIndex(isResearchJob) : whichCoverageIndex(isResearchJob); +} +function whichTotalsIndex(isResearchJob) { + return isResearchJob ? RESEARCH_TOTALS_INDEX : TOTALS_INDEX; +} + +function whichCoverageIndex(isResearchJob) { + return isResearchJob ? RESEARCH_COVERAGE_INDEX : COVERAGE_INDEX; +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index 013adc8b6b0af..2a65839f85ac3 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -47,7 +47,7 @@ describe('Ingesting coverage', () => { describe(`staticSiteUrl`, () => { let actualUrl = ''; - const siteUrlRegex = /staticSiteUrl:\s*(.+,)/; + const siteUrlRegex = /"staticSiteUrl":\s*(.+,)/; beforeAll(async () => { const opts = [...verboseArgs, resolved]; @@ -70,8 +70,8 @@ describe('Ingesting coverage', () => { }); describe(`vcsInfo`, () => { - let vcsInfo; describe(`without a commit msg in the vcs info file`, () => { + let vcsInfo; const args = [ 'scripts/ingest_coverage.js', '--verbose', @@ -93,9 +93,6 @@ describe('Ingesting coverage', () => { }); }); describe(`team assignment`, () => { - let shouldNotHavePipelineOut = ''; - let shouldIndeedHavePipelineOut = ''; - const args = [ 'scripts/ingest_coverage.js', '--verbose', @@ -104,28 +101,26 @@ describe('Ingesting coverage', () => { '--path', ]; - const teamAssignRE = /pipeline:/; - - beforeAll(async () => { - const summaryPath = 'jest-combined/coverage-summary-just-total.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - shouldNotHavePipelineOut = stdout; - }); - beforeAll(async () => { - const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - shouldIndeedHavePipelineOut = stdout; - }); - - it(`should not occur when going to the totals index`, () => { - expect(teamAssignRE.test(shouldNotHavePipelineOut)).to.not.be.ok(); + it(`should not occur when going to the totals index`, async () => { + const teamAssignRE = /"pipeline":/; + const shouldNotHavePipelineOut = await prokJustTotalOrNot(true, args); + const actual = teamAssignRE.test(shouldNotHavePipelineOut); + expect(actual).to.not.be.ok(); }); - it(`should indeed occur when going to the coverage index`, () => { - expect(teamAssignRE.test(shouldIndeedHavePipelineOut)).to.be.ok(); + it(`should indeed occur when going to the coverage index`, async () => { + const shouldIndeedHavePipelineOut = await prokJustTotalOrNot(false, args); + const onlyForTestingRe = /ingest-pipe=>team_assignment/; + const actual = onlyForTestingRe.test(shouldIndeedHavePipelineOut); + expect(actual).to.be.ok(); }); }); }); +async function prokJustTotalOrNot(isTotal, args) { + const justTotalPath = 'jest-combined/coverage-summary-just-total.json'; + const notJustTotalPath = 'jest-combined/coverage-summary-manual-mix.json'; + + const resolved = resolve(MOCKS_DIR, isTotal ? justTotalPath : notJustTotalPath); + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + return stdout; +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-NO-total.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-NO-total.json deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/dev/code_coverage/ingest_coverage/process.js b/src/dev/code_coverage/ingest_coverage/process.js index 6b9c8f09febfe..85a42cfffa6e2 100644 --- a/src/dev/code_coverage/ingest_coverage/process.js +++ b/src/dev/code_coverage/ingest_coverage/process.js @@ -36,13 +36,17 @@ import { import { resolve } from 'path'; import { createReadStream } from 'fs'; import readline from 'readline'; +import * as moment from 'moment'; const ROOT = '../../../..'; const COVERAGE_INGESTION_KIBANA_ROOT = process.env.COVERAGE_INGESTION_KIBANA_ROOT || resolve(__dirname, ROOT); const ms = process.env.DELAY || 0; const staticSiteUrlBase = process.env.STATIC_SITE_URL_BASE || 'https://kibana-coverage.elastic.dev'; -const addPrePopulatedTimeStamp = addTimeStamp(process.env.TIME_STAMP); +const format = 'YYYY-MM-DDTHH:mm:SS'; +// eslint-disable-next-line import/namespace +const formatted = `${moment.utc().format(format)}Z`; +const addPrePopulatedTimeStamp = addTimeStamp(process.env.TIME_STAMP || formatted); const preamble = pipe(statsAndstaticSiteUrl, rootDirAndOrigPath, buildId, addPrePopulatedTimeStamp); const addTestRunnerAndStaticSiteUrl = pipe(testRunner, staticSite(staticSiteUrlBase)); diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh index 2dae75484d68f..d3cf31fc0f427 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh @@ -3,11 +3,14 @@ echo "### Ingesting Code Coverage" echo "" +COVERAGE_JOB_NAME=$1 +export COVERAGE_JOB_NAME +echo "### debug COVERAGE_JOB_NAME: ${COVERAGE_JOB_NAME}" -BUILD_ID=$1 +BUILD_ID=$2 export BUILD_ID -CI_RUN_URL=$2 +CI_RUN_URL=$3 export CI_RUN_URL echo "### debug CI_RUN_URL: ${CI_RUN_URL}" @@ -17,6 +20,9 @@ export ES_HOST STATIC_SITE_URL_BASE='https://kibana-coverage.elastic.dev' export STATIC_SITE_URL_BASE +DELAY=100 +export DELAY + for x in jest functional; do echo "### Ingesting coverage for ${x}" diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 66b16566418b5..e511d7a8fc15e 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -125,31 +125,31 @@ def uploadCombinedReports() { ) } -def ingestData(buildNum, buildUrl, title) { +def ingestData(jobName, buildNum, buildUrl, title) { kibanaPipeline.bash(""" source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline # Using existing target/kibana-coverage folder - . src/dev/code_coverage/shell_scripts/ingest_coverage.sh ${buildNum} ${buildUrl} + . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' """, title) } -def ingestWithVault(buildNum, buildUrl, title) { +def ingestWithVault(jobName, buildNum, buildUrl, title) { def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - ingestData(buildNum, buildUrl, title) + ingestData(jobName, buildNum, buildUrl, title) } } } } -def ingest(timestamp, title) { +def ingest(jobName, buildNumber, buildUrl, timestamp, title) { withEnv([ "TIME_STAMP=${timestamp}", ]) { - ingestWithVault(BUILD_NUMBER, BUILD_URL, title) + ingestWithVault(jobName, buildNumber, buildUrl, title) } } From f428b2dabeafbc269c713f809bb3d59901040179 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 30 Jun 2020 17:07:24 -0600 Subject: [PATCH 02/29] [QA][Code Coverage] Drop catchError and use try / catch instead, (#69198) Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_coverage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index d6600256bab7b..bd55bd73966ff 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -34,7 +34,7 @@ def handleIngestion(timestamp) { def handleFail() { def buildStatus = buildUtils.getBuildStatus() - if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { + if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { slackNotifications.sendFailedBuild( channel: '#kibana-qa', username: 'Kibana QA' From 9af75fa98ba5c212390b37e199bd5ebfa90692f3 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 30 Jun 2020 21:05:14 -0400 Subject: [PATCH 03/29] fix bug to add timeline to case (#70343) --- .../timeline/properties/helpers.test.tsx | 2 +- .../timeline/properties/helpers.tsx | 20 ++++++++++--------- .../timeline/properties/index.test.tsx | 7 ++++++- .../components/timeline/properties/index.tsx | 20 +++++++++---------- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index aec09a95b4b19..887c2e1e825f8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -17,7 +17,7 @@ jest.mock('../../../../common/lib/kibana', () => { useKibana: jest.fn().mockReturnValue({ services: { application: { - navigateToApp: jest.fn(), + navigateToApp: () => Promise.resolve(), capabilities: { siem: { crud: true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 21140d668d716..7b5e9c0c4c949 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -157,19 +157,21 @@ export const NewCase = React.memo( const handleClick = useCallback(() => { onClosePopover(); - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, - }) - ); + dispatch(showTimeline({ id: TimelineId.active, show: false })); navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: getCreateCaseUrl(), - }); + }).then(() => + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }) + ) + ); }, [ dispatch, graphEventId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index cd089d10d5d4c..3a28c26a16c9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -25,7 +25,7 @@ import { act } from 'react-dom/test-utils'; jest.mock('../../../../common/components/link_to'); -const mockNavigateToApp = jest.fn(); +const mockNavigateToApp = jest.fn().mockImplementation(() => Promise.resolve()); jest.mock('../../../../common/lib/kibana', () => { const original = jest.requireActual('../../../../common/lib/kibana'); @@ -369,6 +369,11 @@ describe('Properties', () => { ); wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click'); + + await act(async () => { + await Promise.resolve({}); + }); + expect(mockNavigateToApp).toBeCalledWith('securitySolution:case', { path: '/create' }); expect(mockDispatch).toBeCalledWith( setInsertTimeline({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 40462fa0d09da..b3567151c74b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -125,18 +125,18 @@ export const Properties = React.memo( (id: string) => { onCloseCaseModal(); - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: currentTimeline.savedObjectId, - timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE, - }) - ); - navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: getCaseDetailsUrl({ id }), - }); + }).then(() => + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: currentTimeline.savedObjectId, + timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE, + }) + ) + ); }, [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title] ); From c1dc53c6fbeb75688b742470e6f84a3cea9f8138 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 30 Jun 2020 21:29:37 -0400 Subject: [PATCH 04/29] skip flaky suite (#70386) --- .../overview/monitor_list/__tests__/monitor_list.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx index 29e24a83cfa15..7d09f4161fcac 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx @@ -14,7 +14,8 @@ import { MonitorListComponent, noItemsMessage } from '../monitor_list'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; import * as redux from 'react-redux'; -describe('MonitorList component', () => { +// Failing: See https://github.com/elastic/kibana/issues/70386 +describe.skip('MonitorList component', () => { let result: MonitorSummaryResult; let localStorageMock: any; From e1665e8b27987997896f38c2b734cf3eaf1b2332 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 1 Jul 2020 09:57:23 +0200 Subject: [PATCH 05/29] [Lens] Multiple y axes (#69911) --- .../editor_frame/config_panel/layer_panel.tsx | 2 +- .../__snapshots__/to_expression.test.ts.snap | 1 + .../__snapshots__/xy_expression.test.tsx.snap | 383 ++++++++++++++++-- .../axes_configuration.test.ts | 295 ++++++++++++++ .../xy_visualization/axes_configuration.ts | 106 +++++ .../lens/public/xy_visualization/index.ts | 4 +- .../public/xy_visualization/to_expression.ts | 15 + .../lens/public/xy_visualization/types.ts | 52 ++- .../xy_visualization/xy_config_panel.tsx | 77 +++- .../xy_visualization/xy_expression.test.tsx | 289 ++++++++----- .../public/xy_visualization/xy_expression.tsx | 73 ++-- .../public/xy_visualization/xy_suggestions.ts | 12 +- .../xy_visualization/xy_visualization.tsx | 18 +- 13 files changed, 1141 insertions(+), 186 deletions(-) create mode 100644 x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts create mode 100644 x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index bd501db2b752a..36d5bfd965e26 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -186,7 +186,7 @@ export function LayerPanel( }, ]; - if (activeVisualization.renderDimensionEditor) { + if (activeVisualization.renderDimensionEditor && group.enableDimensionEditor) { tabs.push({ id: 'visualization', name: i18n.translate('xpack.lens.editorFrame.formatStyleLabel', { diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 6b68679bfd4ec..c037aecde558b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -38,6 +38,7 @@ Object { "xScaleType": Array [ "linear", ], + "yConfig": Array [], "yScaleType": Array [ "linear", ], diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap index fc5ed7480dd1f..48c70e0a4a05b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -12,6 +12,11 @@ exports[`xy_expression XYChart component it renders area 1`] = ` showLegend={false} showLegendExtra={false} theme={Object {}} + tooltip={ + Object { + "headerFormatter": [Function], + } + } /> + + + + + + + { + const tables: Record = { + first: { + type: 'kibana_datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date_histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'terms', + indexPatternId: 'indexPatternId', + aggConfigParams: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + formatHint: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'count', + indexPatternId: 'indexPatternId', + aggConfigParams: {}, + }, + formatHint: { id: 'number' }, + }, + { + id: 'yAccessorId2', + name: 'Other column', + meta: { + type: 'average', + indexPatternId: 'indexPatternId', + aggConfigParams: {}, + }, + formatHint: { id: 'bytes' }, + }, + { + id: 'yAccessorId3', + name: 'Other column', + meta: { + type: 'average', + indexPatternId: 'indexPatternId', + aggConfigParams: {}, + }, + formatHint: { id: 'currency' }, + }, + { + id: 'yAccessorId4', + name: 'Other column', + meta: { + type: 'average', + indexPatternId: 'indexPatternId', + aggConfigParams: {}, + }, + formatHint: { id: 'currency' }, + }, + ], + }, + }; + + const sampleLayer: LayerArgs = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['yAccessorId'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + }; + + it('should map auto series to left axis', () => { + const formatFactory = jest.fn(); + const groups = getAxesConfiguration([sampleLayer], tables, formatFactory, false); + expect(groups.length).toEqual(1); + expect(groups[0].position).toEqual('left'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].series[0].layer).toEqual('first'); + }); + + it('should map auto series to right axis if formatters do not match', () => { + const formatFactory = jest.fn(); + const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] }; + const groups = getAxesConfiguration([twoSeriesLayer], tables, formatFactory, false); + expect(groups.length).toEqual(2); + expect(groups[0].position).toEqual('left'); + expect(groups[1].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId2'); + }); + + it('should map auto series to left if left and right are already filled with non-matching series', () => { + const formatFactory = jest.fn(); + const threeSeriesLayer = { + ...sampleLayer, + accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'], + }; + const groups = getAxesConfiguration([threeSeriesLayer], tables, formatFactory, false); + expect(groups.length).toEqual(2); + expect(groups[0].position).toEqual('left'); + expect(groups[1].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].series[1].accessor).toEqual('yAccessorId3'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId2'); + }); + + it('should map right series to right axis', () => { + const formatFactory = jest.fn(); + const groups = getAxesConfiguration( + [{ ...sampleLayer, yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }] }], + tables, + formatFactory, + false + ); + expect(groups.length).toEqual(1); + expect(groups[0].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].series[0].layer).toEqual('first'); + }); + + it('should map series with matching formatters to same axis', () => { + const formatFactory = jest.fn(); + const groups = getAxesConfiguration( + [ + { + ...sampleLayer, + accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'], + yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }], + }, + ], + tables, + formatFactory, + false + ); + expect(groups.length).toEqual(2); + expect(groups[0].position).toEqual('left'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId3'); + expect(groups[0].series[1].accessor).toEqual('yAccessorId4'); + expect(groups[1].position).toEqual('right'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId'); + expect(formatFactory).toHaveBeenCalledWith({ id: 'number' }); + expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' }); + }); + + it('should create one formatter per series group', () => { + const formatFactory = jest.fn(); + getAxesConfiguration( + [ + { + ...sampleLayer, + accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'], + yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }], + }, + ], + tables, + formatFactory, + false + ); + expect(formatFactory).toHaveBeenCalledTimes(2); + expect(formatFactory).toHaveBeenCalledWith({ id: 'number' }); + expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts new file mode 100644 index 0000000000000..7d1d3389bb916 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LayerConfig } from './types'; +import { + KibanaDatatable, + SerializedFieldFormat, +} from '../../../../../src/plugins/expressions/public'; +import { IFieldFormat } from '../../../../../src/plugins/data/public'; + +interface FormattedMetric { + layer: string; + accessor: string; + fieldFormat: SerializedFieldFormat; +} + +type GroupsConfiguration = Array<{ + groupId: string; + position: 'left' | 'right' | 'bottom' | 'top'; + formatter: IFieldFormat; + series: Array<{ layer: string; accessor: string }>; +}>; + +export function isFormatterCompatible( + formatter1: SerializedFieldFormat, + formatter2: SerializedFieldFormat +) { + return formatter1.id === formatter2.id; +} + +export function getAxesConfiguration( + layers: LayerConfig[], + tables: Record, + formatFactory: (mapping: SerializedFieldFormat) => IFieldFormat, + shouldRotate: boolean +): GroupsConfiguration { + const series: { auto: FormattedMetric[]; left: FormattedMetric[]; right: FormattedMetric[] } = { + auto: [], + left: [], + right: [], + }; + + layers.forEach((layer) => { + const table = tables[layer.layerId]; + layer.accessors.forEach((accessor) => { + const mode = + layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || + 'auto'; + const formatter: SerializedFieldFormat = table.columns.find( + (column) => column.id === accessor + )?.formatHint || { id: 'number' }; + series[mode].push({ + layer: layer.layerId, + accessor, + fieldFormat: formatter, + }); + }); + }); + + series.auto.forEach((currentSeries) => { + if ( + series.left.length === 0 || + series.left.every((leftSeries) => + isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) + ) + ) { + series.left.push(currentSeries); + } else if ( + series.right.length === 0 || + series.right.every((rightSeries) => + isFormatterCompatible(rightSeries.fieldFormat, currentSeries.fieldFormat) + ) + ) { + series.right.push(currentSeries); + } else if (series.right.length >= series.left.length) { + series.left.push(currentSeries); + } else { + series.right.push(currentSeries); + } + }); + + const axisGroups: GroupsConfiguration = []; + + if (series.left.length > 0) { + axisGroups.push({ + groupId: 'left', + position: shouldRotate ? 'bottom' : 'left', + formatter: formatFactory(series.left[0].fieldFormat), + series: series.left.map(({ fieldFormat, ...currentSeries }) => currentSeries), + }); + } + + if (series.right.length > 0) { + axisGroups.push({ + groupId: 'right', + position: shouldRotate ? 'top' : 'right', + formatter: formatFactory(series.right[0].fieldFormat), + series: series.right.map(({ fieldFormat, ...currentSeries }) => currentSeries), + }); + } + + return axisGroups; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index cd25cb5729511..88a60089f6a24 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -11,7 +11,7 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public' import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; -import { legendConfig, xConfig, layerConfig } from './types'; +import { legendConfig, layerConfig, yAxisConfig } from './types'; import { EditorFrameSetup, FormatFactory } from '../types'; export interface XyVisualizationPluginSetupPlugins { @@ -37,7 +37,7 @@ export class XyVisualization { { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => legendConfig); - expressions.registerFunction(() => xConfig); + expressions.registerFunction(() => yAxisConfig); expressions.registerFunction(() => layerConfig); expressions.registerFunction(() => xyChart); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index e02d135d9a455..6ec22270d8b18 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -179,6 +179,21 @@ export const buildExpression = ( ], isHistogram: [isHistogramDimension], splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [], + yConfig: layer.yConfig + ? layer.yConfig.map((yConfig) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_yConfig', + arguments: { + forAccessor: [yConfig.forAccessor], + axisMode: [yConfig.axisMode], + }, + }, + ], + })) + : [], seriesType: [layer.seriesType], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 7a5837d382c7b..e62c5f60a58e1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -77,37 +77,33 @@ const axisConfig: { [key in keyof AxisConfig]: ArgumentType } = }, }; -export interface YState extends AxisConfig { - accessors: string[]; -} - -export interface XConfig extends AxisConfig { - accessor: string; -} +type YConfigResult = YConfig & { type: 'lens_xy_yConfig' }; -type XConfigResult = XConfig & { type: 'lens_xy_xConfig' }; - -export const xConfig: ExpressionFunctionDefinition< - 'lens_xy_xConfig', +export const yAxisConfig: ExpressionFunctionDefinition< + 'lens_xy_yConfig', null, - XConfig, - XConfigResult + YConfig, + YConfigResult > = { - name: 'lens_xy_xConfig', + name: 'lens_xy_yConfig', aliases: [], - type: 'lens_xy_xConfig', - help: `Configure the xy chart's x axis`, + type: 'lens_xy_yConfig', + help: `Configure the behavior of a xy chart's y axis metric`, inputTypes: ['null'], args: { - ...axisConfig, - accessor: { + forAccessor: { types: ['string'], - help: 'The column to display on the x axis.', + help: 'The accessor this configuration is for', + }, + axisMode: { + types: ['string'], + options: ['auto', 'left', 'right'], + help: 'The axis mode of the metric', }, }, - fn: function fn(input: unknown, args: XConfig) { + fn: function fn(input: unknown, args: YConfig) { return { - type: 'lens_xy_xConfig', + type: 'lens_xy_yConfig', ...args, }; }, @@ -166,6 +162,12 @@ export const layerConfig: ExpressionFunctionDefinition< help: 'The columns to display on the y axis.', multi: true, }, + yConfig: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + types: ['lens_xy_yConfig' as any], + help: 'Additional configuration for y axes', + multi: true, + }, columnToLabel: { types: ['string'], help: 'JSON key-value pairs of column ID to label', @@ -188,11 +190,19 @@ export type SeriesType = | 'bar_horizontal_stacked' | 'area_stacked'; +export type YAxisMode = 'auto' | 'left' | 'right'; + +export interface YConfig { + forAccessor: string; + axisMode?: YAxisMode; +} + export interface LayerConfig { hide?: boolean; layerId: string; xAccessor?: string; accessors: string[]; + yConfig?: YConfig[]; seriesType: SeriesType; splitAccessor?: string; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 0ea44e469f8dd..3e73cd256bdbf 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; -import { State, SeriesType, visualizationTypes } from './types'; -import { VisualizationLayerWidgetProps } from '../types'; +import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; +import { State, SeriesType, visualizationTypes, YAxisMode } from './types'; +import { VisualizationDimensionEditorProps, VisualizationLayerWidgetProps } from '../types'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -68,3 +67,73 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { ); } + +const idPrefix = htmlIdGenerator()(); + +export function DimensionEditor({ + state, + setState, + layerId, + accessor, +}: VisualizationDimensionEditorProps) { + const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index]; + const axisMode = + (layer.yConfig && + layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) || + 'auto'; + return ( + + { + const newMode = id.replace(idPrefix, '') as YAxisMode; + const newYAxisConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYAxisConfigs.findIndex( + (yAxisConfig) => yAxisConfig.forAccessor === accessor + ); + if (existingIndex !== -1) { + newYAxisConfigs[existingIndex].axisMode = newMode; + } else { + newYAxisConfigs.push({ + forAccessor: accessor, + axisMode: newMode, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index)); + }} + /> + + ); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index b2d9f6acfc9f5..34f2a9111253b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -280,6 +280,58 @@ describe('xy_expression', () => { let getFormatSpy: jest.Mock; let convertSpy: jest.Mock; + const dataWithoutFormats: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a' }, + { id: 'b', name: 'b' }, + { id: 'c', name: 'c' }, + { id: 'd', name: 'd' }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], + }, + }, + }; + const dataWithFormats: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a' }, + { id: 'b', name: 'b' }, + { id: 'c', name: 'c' }, + { id: 'd', name: 'd', formatHint: { id: 'custom' } }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], + }, + }, + }; + + const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { + return shallow( + + ); + }; + beforeEach(() => { convertSpy = jest.fn((x) => x); getFormatSpy = jest.fn(); @@ -302,7 +354,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(LineSeries)).toHaveLength(1); + expect(component.find(LineSeries)).toHaveLength(2); + expect(component.find(LineSeries).at(0).prop('yAccessors')).toEqual(['a']); + expect(component.find(LineSeries).at(1).prop('yAccessors')).toEqual(['b']); }); describe('date range', () => { @@ -559,7 +613,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); + expect(component.find(BarSeries)).toHaveLength(2); + expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']); + expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']); }); test('it renders area', () => { @@ -577,7 +633,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(AreaSeries)).toHaveLength(1); + expect(component.find(AreaSeries)).toHaveLength(2); + expect(component.find(AreaSeries).at(0).prop('yAccessors')).toEqual(['a']); + expect(component.find(AreaSeries).at(1).prop('yAccessors')).toEqual(['b']); }); test('it renders horizontal bar', () => { @@ -595,7 +653,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); + expect(component.find(BarSeries)).toHaveLength(2); + expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']); + expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']); expect(component.find(Settings).prop('rotation')).toEqual(90); }); @@ -705,8 +765,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); - expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); + expect(component.find(BarSeries)).toHaveLength(2); + expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1); + expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1); }); test('it renders stacked area', () => { @@ -724,8 +785,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(AreaSeries)).toHaveLength(1); - expect(component.find(AreaSeries).prop('stackAccessors')).toHaveLength(1); + expect(component.find(AreaSeries)).toHaveLength(2); + expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toHaveLength(1); + expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toHaveLength(1); }); test('it renders stacked horizontal bar', () => { @@ -746,8 +808,9 @@ describe('xy_expression', () => { /> ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); - expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); + expect(component.find(BarSeries)).toHaveLength(2); + expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1); + expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1); expect(component.find(Settings).prop('rotation')).toEqual(90); }); @@ -765,7 +828,8 @@ describe('xy_expression', () => { onSelectRange={onSelectRange} /> ); - expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); + expect(component.find(LineSeries).at(0).prop('timeZone')).toEqual('CEST'); + expect(component.find(LineSeries).at(1).prop('timeZone')).toEqual('CEST'); }); test('it applies histogram mode to the series for single series', () => { @@ -784,7 +848,8 @@ describe('xy_expression', () => { onSelectRange={onSelectRange} /> ); - expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); + expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true); + expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(true); }); test('it applies histogram mode to the series for stacked series', () => { @@ -810,7 +875,8 @@ describe('xy_expression', () => { onSelectRange={onSelectRange} /> ); - expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); + expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true); + expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(true); }); test('it does not apply histogram mode for splitted series', () => { @@ -830,47 +896,104 @@ describe('xy_expression', () => { onSelectRange={onSelectRange} /> ); - expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); + expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false); + expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false); }); - describe('provides correct series naming', () => { - const dataWithoutFormats: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'kibana_datatable', - columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, - { id: 'd', name: 'd' }, - ], - rows: [ - { a: 1, b: 2, c: 'I', d: 'Row 1' }, - { a: 1, b: 5, c: 'J', d: 'Row 2' }, - ], - }, - }, - }; - const dataWithFormats: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'kibana_datatable', - columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, - { id: 'd', name: 'd', formatHint: { id: 'custom' } }, - ], - rows: [ - { a: 1, b: 2, c: 'I', d: 'Row 1' }, - { a: 1, b: 5, c: 'J', d: 'Row 2' }, - ], - }, - }, - }; + describe('y axes', () => { + test('single axis if possible', () => { + const args = createArgsWithLayers(); + + const component = getRenderedComponent(dataWithoutFormats, args); + const axes = component.find(Axis); + expect(axes).toHaveLength(2); + }); + + test('multiple axes because of config', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + yConfig: [ + { + forAccessor: 'a', + axisMode: 'left', + }, + { + forAccessor: 'b', + axisMode: 'right', + }, + ], + }, + ], + } as XYArgs; + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const axes = component.find(Axis); + expect(axes).toHaveLength(3); + expect(component.find(LineSeries).at(0).prop('groupId')).toEqual( + axes.at(1).prop('groupId') + ); + expect(component.find(LineSeries).at(1).prop('groupId')).toEqual( + axes.at(2).prop('groupId') + ); + }); + + test('multiple axes because of incompatible formatters', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['c', 'd'], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithFormats, newArgs); + const axes = component.find(Axis); + expect(axes).toHaveLength(3); + expect(component.find(LineSeries).at(0).prop('groupId')).toEqual( + axes.at(1).prop('groupId') + ); + expect(component.find(LineSeries).at(1).prop('groupId')).toEqual( + axes.at(2).prop('groupId') + ); + }); + + test('single axis despite different formatters if enforced', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['c', 'd'], + yConfig: [ + { + forAccessor: 'c', + axisMode: 'left', + }, + { + forAccessor: 'd', + axisMode: 'left', + }, + ], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const axes = component.find(Axis); + expect(axes).toHaveLength(2); + }); + }); + + describe('provides correct series naming', () => { const nameFnArgs = { seriesKeys: [], key: '', @@ -879,21 +1002,6 @@ describe('xy_expression', () => { splitAccessors: new Map(), }; - const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { - return shallow( - - ); - }; - test('simplest xy chart without human-readable name', () => { const args = createArgsWithLayers(); const newArgs = { @@ -973,13 +1081,14 @@ describe('xy_expression', () => { }; const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; + const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn; // This accessor has a human-readable name - expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A'); + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A'); // This accessor does not - expect(nameFn({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(''); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + expect(nameFn2({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(''); + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); }); test('split series without formatting and single y accessor', () => { @@ -1039,9 +1148,13 @@ describe('xy_expression', () => { }; const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; + const nameFn2 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( + 'split1 - Label A' + ); + expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( 'split1 - Label B' ); }); @@ -1061,13 +1174,14 @@ describe('xy_expression', () => { }; const component = getRenderedComponent(dataWithFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; + const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn; convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( 'formatted1 - Label A' ); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( 'formatted2 - Label B' ); }); @@ -1088,7 +1202,8 @@ describe('xy_expression', () => { onSelectRange={onSelectRange} /> ); - expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); + expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal); + expect(component.find(LineSeries).at(1).prop('xScaleType')).toEqual(ScaleType.Ordinal); }); test('it set the scale of the y axis according to the args prop', () => { @@ -1106,7 +1221,8 @@ describe('xy_expression', () => { onSelectRange={onSelectRange} /> ); - expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); + expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt); + expect(component.find(LineSeries).at(1).prop('yScaleType')).toEqual(ScaleType.Sqrt); }); test('it gets the formatter for the x axis', () => { @@ -1128,25 +1244,6 @@ describe('xy_expression', () => { expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' }); }); - test('it gets a default formatter for y if there are multiple y accessors', () => { - const { data, args } = sampleArgs(); - - shallow( - - ); - - expect(getFormatSpy).toHaveBeenCalledWith({ id: 'number' }); - }); - test('it gets the formatter for the y axis if there is only one accessor', () => { const { data, args } = sampleArgs(); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 003036b211f03..17ed04aa0e9c4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -40,6 +40,7 @@ import { isHorizontalChart } from './state_helpers'; import { parseInterval } from '../../../../../src/plugins/data/common'; import { EmptyPlaceholder } from '../shared_components'; import { desanitizeFilterContext } from '../utils'; +import { getAxesConfiguration } from './axes_configuration'; type InferPropType = T extends React.FunctionComponent ? P : T; type SeriesSpec = InferPropType & @@ -213,23 +214,19 @@ export function XYChart({ ); const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint); - // use default number formatter for y axis and use formatting hint if there is just a single y column - let yAxisFormatter = formatFactory({ id: 'number' }); - if (filteredLayers.length === 1 && filteredLayers[0].accessors.length === 1) { - const firstYAxisColumn = Object.values(data.tables)[0].columns.find( - ({ id }) => id === filteredLayers[0].accessors[0] - ); - if (firstYAxisColumn && firstYAxisColumn.formatHint) { - yAxisFormatter = formatFactory(firstYAxisColumn.formatHint); - } - } - const chartHasMoreThanOneSeries = filteredLayers.length > 1 || filteredLayers.some((layer) => layer.accessors.length > 1) || filteredLayers.some((layer) => layer.splitAccessor); const shouldRotate = isHorizontalChart(filteredLayers); + const yAxesConfiguration = getAxesConfiguration( + filteredLayers, + data.tables, + formatFactory, + shouldRotate + ); + const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle; function calculateMinInterval() { @@ -279,6 +276,9 @@ export function XYChart({ legendPosition={legend.position} showLegendExtra={false} theme={chartTheme} + tooltip={{ + headerFormatter: (d) => xAxisFormatter.convert(d.value), + }} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} onBrushEnd={({ x }) => { @@ -368,18 +368,30 @@ export function XYChart({ tickFormat={(d) => xAxisFormatter.convert(d)} /> - yAxisFormatter.convert(d)} - /> + {yAxesConfiguration.map((axis, index) => ( + + data.tables[series.layer].columns.find((column) => column.id === series.accessor) + ?.name + ) + .filter((name) => Boolean(name))[0] || args.yTitle + } + showGridLines={false} + hide={filteredLayers[0].hide} + tickFormat={(d) => axis.formatter.convert(d)} + /> + ))} - {filteredLayers.map( - ( - { + {filteredLayers.flatMap((layer, layerIndex) => + layer.accessors.map((accessor, accessorIndex) => { + const { splitAccessor, seriesType, accessors, @@ -389,9 +401,7 @@ export function XYChart({ yScaleType, xScaleType, isHistogram, - }, - index - ) => { + } = layer; const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; @@ -407,19 +417,22 @@ export function XYChart({ !( splitAccessor && typeof row[splitAccessor] === 'undefined' && - accessors.every((accessor) => typeof row[accessor] === 'undefined') + typeof row[accessor] === 'undefined' ) ); const seriesProps: SeriesSpec = { splitSeriesAccessors: splitAccessor ? [splitAccessor] : [], stackAccessors: seriesType.includes('stacked') ? [xAccessor as string] : [], - id: splitAccessor || accessors.join(','), + id: `${splitAccessor}-${accessor}`, xAccessor, - yAccessors: accessors, + yAccessors: [accessor], data: rows, xScaleType, yScaleType, + groupId: yAxesConfiguration.find((axisConfiguration) => + axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) + )?.groupId, enableHistogramMode: isHistogram && (seriesType.includes('stacked') || !splitAccessor), timeZone, name(d) { @@ -451,6 +464,8 @@ export function XYChart({ }, }; + const index = `${layerIndex}-${accessorIndex}`; + switch (seriesType) { case 'line': return ; @@ -462,7 +477,7 @@ export function XYChart({ default: return ; } - } + }) )} ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index ffbd3b7e2c1f2..9d0ebbb389c07 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -14,7 +14,7 @@ import { TableSuggestion, TableChangeType, } from '../types'; -import { State, SeriesType, XYState, visualizationTypes } from './types'; +import { State, SeriesType, XYState, visualizationTypes, LayerConfig } from './types'; import { getIconForSeries } from './state_helpers'; const columnSortOrder = { @@ -379,13 +379,19 @@ function buildSuggestion({ changeType: TableChangeType; keptLayerIds: string[]; }) { + const existingLayer: LayerConfig | {} = getExistingLayer(currentState, layerId) || {}; + const accessors = yValues.map((col) => col.columnId); const newLayer = { - ...(getExistingLayer(currentState, layerId) || {}), + ...existingLayer, layerId, seriesType, xAccessor: xValue.columnId, splitAccessor: splitBy?.columnId, - accessors: yValues.map((col) => col.columnId), + accessors, + yConfig: + 'yConfig' in existingLayer && existingLayer.yConfig + ? existingLayer.yConfig.filter(({ forAccessor }) => accessors.indexOf(forAccessor) !== -1) + : undefined, }; const keptLayers = currentState diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index ffacfbf8555eb..474ea5c5b08cd 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -11,13 +11,13 @@ import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; -import { LayerContextMenu } from './xy_config_panel'; +import { DimensionEditor, LayerContextMenu } from './xy_config_panel'; import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; -import { toExpression, toPreviewExpression } from './to_expression'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; +import { toExpression, toPreviewExpression } from './to_expression'; const defaultIcon = chartBarStackedSVG; const defaultSeriesType = 'bar_stacked'; @@ -187,6 +187,7 @@ export const xyVisualization: Visualization = { supportsMoreColumns: true, required: true, dataTestSubj: 'lnsXY_yDimensionPanel', + enableDimensionEditor: true, }, { groupId: 'breakdown', @@ -239,6 +240,10 @@ export const xyVisualization: Visualization = { newLayer.accessors = newLayer.accessors.filter((a) => a !== columnId); } + if (newLayer.yConfig) { + newLayer.yConfig = newLayer.yConfig.filter(({ forAccessor }) => forAccessor !== columnId); + } + return { ...prevState, layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)), @@ -259,6 +264,15 @@ export const xyVisualization: Visualization = { ); }, + renderDimensionEditor(domElement, props) { + render( + + + , + domElement + ); + }, + toExpression, toPreviewExpression, }; From 6e268898f9f5592def9200c6d92c33bb6d75fd4d Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 1 Jul 2020 10:01:32 +0200 Subject: [PATCH 06/29] chore: add missing mjs extension (#70326) --- src/dev/jest/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 391a52b7f0397..e11668ab57f55 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -84,7 +84,7 @@ export default { moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testEnvironment: 'jest-environment-jsdom-thirteen', - testMatch: ['**/*.test.{js,ts,tsx}'], + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ '/packages/kbn-ui-framework/(dist|doc_site|generator-kui)/', '/packages/kbn-pm/dist/', From a49f5cec64024ceb1807b3e4a71ff6c642b9bf40 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 1 Jul 2020 10:07:59 +0200 Subject: [PATCH 07/29] [Lens] Move chart switcher over (#70182) --- .../editor_frame/config_panel/_index.scss | 1 - .../config_panel/config_panel.tsx | 18 +-------- .../chart_switch.scss} | 5 +-- .../chart_switch.test.tsx | 0 .../chart_switch.tsx | 10 ++--- .../editor_frame/workspace_panel/index.ts | 7 ++++ .../workspace_panel.test.tsx | 20 +++++----- .../{ => workspace_panel}/workspace_panel.tsx | 27 +++++++------ .../workspace_panel_wrapper.test.tsx | 14 +++++-- .../workspace_panel_wrapper.tsx | 38 ++++++++++++++++--- 10 files changed, 83 insertions(+), 57 deletions(-) rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{config_panel/_chart_switch.scss => workspace_panel/chart_switch.scss} (86%) rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{config_panel => workspace_panel}/chart_switch.test.tsx (100%) rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{config_panel => workspace_panel}/chart_switch.tsx (98%) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{ => workspace_panel}/workspace_panel.test.tsx (97%) rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{ => workspace_panel}/workspace_panel.tsx (91%) rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{ => workspace_panel}/workspace_panel_wrapper.test.tsx (82%) rename x-pack/plugins/lens/public/editor_frame_service/editor_frame/{ => workspace_panel}/workspace_panel_wrapper.tsx (69%) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss index 8f09a358dd5e4..5b968abd0c061 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss @@ -1,4 +1,3 @@ -@import 'chart_switch'; @import 'config_panel'; @import 'dimension_popover'; @import 'layer_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index e53e465c18950..7f4a48fa2fda2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -8,7 +8,6 @@ import React, { useMemo, memo } from 'react'; import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Visualization } from '../../../types'; -import { ChartSwitch } from './chart_switch'; import { LayerPanel } from './layer_panel'; import { trackUiEvent } from '../../../lens_ui_telemetry'; import { generateId } from '../../../id_generator'; @@ -20,21 +19,8 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config const { visualizationState } = props; return ( - <> - - {activeVisualization && visualizationState && ( - - )} - + activeVisualization && + visualizationState && ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss similarity index 86% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss index d7efab2405f3f..ae4a7861b1d90 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss @@ -1,6 +1,4 @@ .lnsChartSwitch__header { - padding: $euiSizeS 0; - > * { display: flex; align-items: center; @@ -9,7 +7,8 @@ .lnsChartSwitch__triggerButton { @include euiTitle('xs'); - line-height: $euiSizeXXL; + background-color: $euiColorEmptyShade; + border-color: $euiColorLightShade; } .lnsChartSwitch__summaryIcon { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx similarity index 98% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index e212cb70d1855..4c5a44ecc695e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -11,7 +11,7 @@ import { EuiPopoverTitle, EuiKeyPadMenu, EuiKeyPadMenuItem, - EuiButtonEmpty, + EuiButton, } from '@elastic/eui'; import { flatten } from 'lodash'; import { i18n } from '@kbn/i18n'; @@ -72,6 +72,8 @@ function VisualizationSummary(props: Props) { ); } +import './chart_switch.scss'; + export function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); @@ -198,20 +200,18 @@ export function ChartSwitch(props: Props) { ownFocus initialFocus=".lnsChartSwitch__popoverPanel" panelClassName="lnsChartSwitch__popoverPanel" - anchorClassName="eui-textTruncate" panelPaddingSize="s" button={ - setFlyoutOpen(!flyoutOpen)} data-test-subj="lnsChartSwitchPopover" - flush="left" iconSide="right" iconType="arrowDown" color="text" > - + } isOpen={flyoutOpen} closePopover={() => setFlyoutOpen(false)} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts new file mode 100644 index 0000000000000..d23afd4129cbe --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WorkspacePanel } from './workspace_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx similarity index 97% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 49d12e9f41440..a9c638df8cad1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -6,19 +6,19 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererProps } from '../../../../../../src/plugins/expressions/public'; -import { FramePublicAPI, TableSuggestion, Visualization } from '../../types'; +import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; +import { FramePublicAPI, TableSuggestion, Visualization } from '../../../types'; import { createMockVisualization, createMockDatasource, createExpressionRendererMock, DatasourceMock, createMockFramePublicAPI, -} from '../mocks'; +} from '../../mocks'; import { InnerWorkspacePanel, WorkspacePanelProps } from './workspace_panel'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; -import { DragDrop, ChildDragDropProvider } from '../../drag_drop'; +import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { Ast } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; import { @@ -26,12 +26,12 @@ import { esFilters, IFieldType, IIndexPattern, -} from '../../../../../../src/plugins/data/public'; -import { TriggerId, UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; -import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; -import { TriggerContract } from '../../../../../../src/plugins/ui_actions/public/triggers'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +} from '../../../../../../../src/plugins/data/public'; +import { TriggerId, UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; +import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; +import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; describe('workspace_panel', () => { let mockVisualization: jest.Mocked; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx similarity index 91% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 670afe28293a4..beb6952556067 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -20,23 +20,23 @@ import { CoreStart, CoreSetup } from 'kibana/public'; import { ExpressionRendererEvent, ReactExpressionRendererType, -} from '../../../../../../src/plugins/expressions/public'; -import { Action } from './state_management'; +} from '../../../../../../../src/plugins/expressions/public'; +import { Action } from '../state_management'; import { Datasource, Visualization, FramePublicAPI, isLensBrushEvent, isLensFilterEvent, -} from '../../types'; -import { DragDrop, DragContext } from '../../drag_drop'; -import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; -import { buildExpression } from './expression_helpers'; -import { debouncedComponent } from '../../debounced_component'; -import { trackUiEvent } from '../../lens_ui_telemetry'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; -import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +} from '../../../types'; +import { DragDrop, DragContext } from '../../../drag_drop'; +import { getSuggestions, switchToSuggestion } from '../suggestion_helpers'; +import { buildExpression } from '../expression_helpers'; +import { debouncedComponent } from '../../../debounced_component'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; +import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; export interface WorkspacePanelProps { @@ -300,7 +300,10 @@ export function InnerWorkspacePanel({ dispatch={dispatch} emptyExpression={expression === null} visualizationState={visualizationState} - activeVisualization={activeVisualization} + visualizationId={activeVisualizationId} + datasourceStates={datasourceStates} + datasourceMap={datasourceMap} + visualizationMap={visualizationMap} > { dispatch={jest.fn()} framePublicAPI={mockFrameAPI} visualizationState={{}} - activeVisualization={mockVisualization} + visualizationId="myVis" + visualizationMap={{ myVis: mockVisualization }} + datasourceMap={{}} + datasourceStates={{}} emptyExpression={false} > @@ -51,7 +54,10 @@ describe('workspace_panel_wrapper', () => { framePublicAPI={mockFrameAPI} visualizationState={visState} children={} - activeVisualization={{ ...mockVisualization, renderToolbar: renderToolbarMock }} + visualizationId="myVis" + visualizationMap={{ myVis: { ...mockVisualization, renderToolbar: renderToolbarMock } }} + datasourceMap={{}} + datasourceStates={{}} emptyExpression={false} /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx similarity index 69% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 17461b9fc274f..60c31e5d090e5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -14,29 +14,43 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { FramePublicAPI, Visualization } from '../../types'; -import { NativeRenderer } from '../../native_renderer'; -import { Action } from './state_management'; +import { Datasource, FramePublicAPI, Visualization } from '../../../types'; +import { NativeRenderer } from '../../../native_renderer'; +import { Action } from '../state_management'; +import { ChartSwitch } from './chart_switch'; export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; framePublicAPI: FramePublicAPI; visualizationState: unknown; - activeVisualization: Visualization | null; dispatch: (action: Action) => void; emptyExpression: boolean; title?: string; + visualizationMap: Record; + visualizationId: string | null; + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; } export function WorkspacePanelWrapper({ children, framePublicAPI, visualizationState, - activeVisualization, dispatch, title, emptyExpression, + visualizationId, + visualizationMap, + datasourceMap, + datasourceStates, }: WorkspacePanelWrapperProps) { + const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null; const setVisualizationState = useCallback( (newState: unknown) => { if (!activeVisualization) { @@ -52,7 +66,19 @@ export function WorkspacePanelWrapper({ [dispatch] ); return ( - + + + + {activeVisualization && activeVisualization.renderToolbar && ( Date: Wed, 1 Jul 2020 10:08:37 +0200 Subject: [PATCH 08/29] [Lens] Add "no data" popover (#69147) --- ...na-plugin-plugins-data-public.searchbar.md | 4 +- src/plugins/data/public/public.api.md | 4 +- .../no_data_popover.test.tsx | 95 ++++++++++++++++++ .../ui/query_string_input/no_data_popover.tsx | 96 +++++++++++++++++++ .../query_string_input/query_bar_top_row.tsx | 12 ++- .../ui/search_bar/create_search_bar.tsx | 1 + .../data/public/ui/search_bar/search_bar.tsx | 2 + test/functional/page_objects/time_picker.ts | 7 ++ .../lens/public/app_plugin/app.test.tsx | 1 + x-pack/plugins/lens/public/app_plugin/app.tsx | 21 ++++ .../editor_frame/data_panel_wrapper.tsx | 2 + .../editor_frame/editor_frame.test.tsx | 1 + .../editor_frame/editor_frame.tsx | 2 + .../editor_frame/state_management.test.ts | 1 + .../editor_frame_service/service.test.tsx | 2 + .../public/editor_frame_service/service.tsx | 6 +- .../__mocks__/loader.ts | 1 + .../datapanel.test.tsx | 7 +- .../indexpattern_datasource/datapanel.tsx | 6 +- .../dimension_panel/dimension_panel.test.tsx | 2 + .../indexpattern.test.ts | 5 + .../indexpattern_suggestions.test.tsx | 8 ++ .../layerpanel.test.tsx | 1 + .../indexpattern_datasource/loader.test.ts | 50 +++++++++- .../public/indexpattern_datasource/loader.ts | 18 ++++ .../definitions/date_histogram.test.tsx | 1 + .../operations/definitions/terms.test.tsx | 1 + .../operations/operations.test.ts | 1 + .../state_helpers.test.ts | 8 ++ .../public/indexpattern_datasource/types.ts | 1 + x-pack/plugins/lens/public/types.ts | 2 + .../apps/lens/persistent_context.ts | 6 +- .../test/functional/page_objects/lens_page.ts | 5 +- 33 files changed, 359 insertions(+), 21 deletions(-) create mode 100644 src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx create mode 100644 src/plugins/data/public/ui/query_string_input/no_data_popover.tsx diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index fc141b8c89c18..498691c06285d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f2c7a907cda1d..f19611bc1d526 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1748,8 +1748,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx new file mode 100644 index 0000000000000..27f924d98e6eb --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { NoDataPopover } from './no_data_popover'; +import { EuiTourStep } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; + +describe('NoDataPopover', () => { + const createMockStorage = () => ({ + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + + it('should hide popover if showNoDataPopover is set to false', () => { + const Child = () => ; + const instance = mount( + + + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + expect(instance.find(EuiTourStep).find(Child)).toHaveLength(1); + }); + + it('should hide popover if showNoDataPopover is set to true, but local storage flag is set', () => { + const child = ; + const storage = createMockStorage(); + storage.get.mockReturnValue(true); + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should render popover if showNoDataPopover is set to true and local storage flag is not set', () => { + const child = ; + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(true); + }); + + it('should hide popover if it is closed', async () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('closePopover')!(); + }); + instance.setProps({ ...props }); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should set local storage flag and hide on closing with button', () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('footerAction')!.props.onClick(); + }); + instance.setProps({ ...props }); + expect(props.storage.set).toHaveBeenCalledWith(expect.any(String), true); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); +}); diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx new file mode 100644 index 0000000000000..302477a5fff5e --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ReactElement, useEffect, useState } from 'react'; +import React from 'react'; +import { EuiButtonEmpty, EuiText, EuiTourStep } from '@elastic/eui'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { i18n } from '@kbn/i18n'; + +const NO_DATA_POPOVER_STORAGE_KEY = 'data.noDataPopover'; + +export function NoDataPopover({ + showNoDataPopover, + storage, + children, +}: { + showNoDataPopover?: boolean; + storage: IStorageWrapper; + children: ReactElement; +}) { + const [noDataPopoverDismissed, setNoDataPopoverDismissed] = useState(() => + Boolean(storage.get(NO_DATA_POPOVER_STORAGE_KEY)) + ); + const [noDataPopoverVisible, setNoDataPopoverVisible] = useState(false); + + useEffect(() => { + if (showNoDataPopover && !noDataPopoverDismissed) { + setNoDataPopoverVisible(true); + } + }, [noDataPopoverDismissed, showNoDataPopover]); + + return ( + {}} + closePopover={() => { + setNoDataPopoverVisible(false); + }} + content={ + +

+ {i18n.translate('data.noDataPopover.content', { + defaultMessage: + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts", + })} +

+
+ } + minWidth={300} + anchorPosition="downCenter" + step={1} + stepsTotal={1} + isStepOpen={noDataPopoverVisible} + subtitle={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Tip' })} + title="" + footerAction={ + { + storage.set(NO_DATA_POPOVER_STORAGE_KEY, true); + setNoDataPopoverDismissed(true); + setNoDataPopoverVisible(false); + }} + > + {i18n.translate('data.noDataPopover.dismissAction', { + defaultMessage: "Don't show again", + })} + + } + > +
{ + setNoDataPopoverVisible(false); + }} + > + {children} +
+
+ ); +} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index f65bf97e391e2..4b0dc579c39ce 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -40,6 +40,7 @@ import { useKibana, toMountPoint } from '../../../../kibana_react/public'; import { QueryStringInput } from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; +import { NoDataPopover } from './no_data_popover'; interface Props { query?: Query; @@ -63,6 +64,7 @@ interface Props { customSubmitButton?: any; isDirty: boolean; timeHistory?: TimeHistoryContract; + indicateNoData?: boolean; } export function QueryBarTopRow(props: Props) { @@ -230,10 +232,12 @@ export function QueryBarTopRow(props: Props) { } return ( - - {renderDatePicker()} - {button} - + + + {renderDatePicker()} + {button} + + ); } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 81e84e3198072..a0df7604f23aa 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -198,6 +198,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) showSaveQuery={props.showSaveQuery} screenTitle={props.screenTitle} indexPatterns={props.indexPatterns} + indicateNoData={props.indicateNoData} timeHistory={data.query.timefilter.history} dateRangeFrom={timeRange.from} dateRangeTo={timeRange.to} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index a5ac227559115..2f740cc476087 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -75,6 +75,7 @@ export interface SearchBarOwnProps { onClearSavedQuery?: () => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; + indicateNoData?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -402,6 +403,7 @@ class SearchBarUI extends Component { this.props.customSubmitButton ? this.props.customSubmitButton : undefined } dataTestSubj={this.props.dataTestSubj} + indicateNoData={this.props.indicateNoData} /> ); } diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 20ae89fc1a8d0..7ef291c8c7005 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -52,6 +52,13 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo await this.setAbsoluteRange(this.defaultStartTime, this.defaultEndTime); } + async ensureHiddenNoDataPopover() { + const isVisible = await testSubjects.exists('noDataPopoverDismissButton'); + if (isVisible) { + await testSubjects.click('noDataPopoverDismissButton'); + } + } + /** * the provides a quicker way to set the timepicker to the default range, saves a few seconds */ diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index cd6fbf96d6750..3bd12a87456a0 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -226,6 +226,7 @@ describe('Lens App', () => { "query": "", }, "savedQuery": undefined, + "showNoDataPopover": [Function], }, ], ] diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 0ab547bed6d37..9b8b9a8531cf0 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -40,6 +40,7 @@ import { } from '../../../../../src/plugins/data/public'; interface State { + indicateNoData: boolean; isLoading: boolean; isSaveModalVisible: boolean; indexPatternsForTopNav: IndexPatternInstance[]; @@ -97,9 +98,27 @@ export function App({ toDate: currentRange.to, }, filters: [], + indicateNoData: false, }; }); + const showNoDataPopover = useCallback(() => { + setState((prevState) => ({ ...prevState, indicateNoData: true })); + }, [setState]); + + useEffect(() => { + if (state.indicateNoData) { + setState((prevState) => ({ ...prevState, indicateNoData: false })); + } + }, [ + setState, + state.indicateNoData, + state.query, + state.filters, + state.dateRange, + state.indexPatternsForTopNav, + ]); + const { lastKnownDoc } = state; const isSaveable = @@ -458,6 +477,7 @@ export function App({ query={state.query} dateRangeFrom={state.dateRange.fromDate} dateRangeTo={state.dateRange.toDate} + indicateNoData={state.indicateNoData} /> @@ -472,6 +492,7 @@ export function App({ savedQuery: state.savedQuery, doc: state.persistedDoc, onError, + showNoDataPopover, onChange: ({ filterableIndexPatterns, doc }) => { if (!_.isEqual(state.persistedDoc, doc)) { setState((s) => ({ ...s, lastKnownDoc: doc })); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index afb2719f28e89..0f74abe97c418 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -19,6 +19,7 @@ interface DataPanelWrapperProps { activeDatasource: string | null; datasourceIsLoading: boolean; dispatch: (action: Action) => void; + showNoDataPopover: () => void; core: DatasourceDataPanelProps['core']; query: Query; dateRange: FramePublicAPI['dateRange']; @@ -46,6 +47,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { query: props.query, dateRange: props.dateRange, filters: props.filters, + showNoDataPopover: props.showNoDataPopover, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index ff9e24f95d1e2..ad4f6e74c9e92 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -56,6 +56,7 @@ function getDefaultProps() { data: dataPluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), }, + showNoDataPopover: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index af3d0ed068d2f..bcceb1222ce03 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -48,6 +48,7 @@ export interface EditorFrameProps { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; }) => void; + showNoDataPopover: () => void; } export function EditorFrame(props: EditorFrameProps) { @@ -255,6 +256,7 @@ export function EditorFrame(props: EditorFrameProps) { query={props.query} dateRange={props.dateRange} filters={props.filters} + showNoDataPopover={props.showNoDataPopover} /> } configPanel={ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index e1151b92aac51..969467b5789ec 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -35,6 +35,7 @@ describe('editor_frame state management', () => { dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }; }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index fbd65c5044d51..7b1d091c1c8fe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -51,6 +51,7 @@ describe('editor_frame service', () => { dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }); instance.unmount(); })() @@ -70,6 +71,7 @@ describe('editor_frame service', () => { dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }); instance.unmount(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index f57acf3bef62d..47339373b6d1a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -102,7 +102,10 @@ export class EditorFrameService { ]); return { - mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => { + mount: ( + element, + { doc, onError, dateRange, query, filters, savedQuery, onChange, showNoDataPopover } + ) => { domElement = element; const firstDatasourceId = Object.keys(resolvedDatasources)[0]; const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; @@ -127,6 +130,7 @@ export class EditorFrameService { filters={filters} savedQuery={savedQuery} onChange={onChange} + showNoDataPopover={showNoDataPopover} /> , domElement diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index f2fedda1fa353..ca5fe706985f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -19,6 +19,7 @@ export function loadInitialState() { [restricted.id]: restricted, }, layers: {}, + isFirstExistenceFetch: false, }; return result; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 7653dab2c9b84..f70df855fe0cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -204,12 +204,15 @@ const initialState: IndexPatternPrivateState = { ], }, }, + isFirstExistenceFetch: false, }; const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } }; describe('IndexPattern Data Panel', () => { - let defaultProps: Parameters[0]; + let defaultProps: Parameters[0] & { + showNoDataPopover: () => void; + }; let core: ReturnType; beforeEach(() => { @@ -229,6 +232,7 @@ describe('IndexPattern Data Panel', () => { }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }; }); @@ -301,6 +305,7 @@ describe('IndexPattern Data Panel', () => { state: { indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, currentIndexPatternId: 'a', indexPatterns: { a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index b72f87e243dcd..87fbf81fceba0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -82,6 +82,7 @@ export function IndexPatternDataPanel({ filters, dateRange, changeIndexPattern, + showNoDataPopover, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( @@ -116,6 +117,9 @@ export function IndexPatternDataPanel({ syncExistingFields({ dateRange, setState, + isFirstExistenceFetch: state.isFirstExistenceFetch, + currentIndexPatternTitle: indexPatterns[currentIndexPatternId].title, + showNoDataPopover, indexPatterns: indexPatternList, fetchJson: core.http.post, dslQuery, @@ -210,7 +214,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ core, data, existingFields, -}: Pick> & { +}: Omit & { data: DataPublicPluginStart; currentIndexPatternId: string; indexPatternRefs: IndexPatternRef[]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index ee9b6778650ef..a1c084f83e447 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -79,6 +79,7 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternRefs: [], indexPatterns: expectedIndexPatterns, currentIndexPatternId: '1', + isFirstExistenceFetch: false, existingFields: { 'my-fake-index-pattern': { timestamp: true, @@ -1257,6 +1258,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { myLayer: { indexPatternId: 'foo', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index a69d7c055eaa7..6a79ce450cd9a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -127,6 +127,7 @@ function stateFromPersistedState( indexPatterns: expectedIndexPatterns, indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, }; } @@ -401,6 +402,7 @@ describe('IndexPattern Data Source', () => { }, }, currentIndexPatternId: '1', + isFirstExistenceFetch: false, }; expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({ ...state, @@ -421,6 +423,7 @@ describe('IndexPattern Data Source', () => { const state = { indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { @@ -455,6 +458,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource.getLayers({ indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { @@ -480,6 +484,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource.getMetaData({ indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 87d91b56d2a5c..b6246c6e91e7e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -146,6 +146,7 @@ function testInitialState(): IndexPatternPrivateState { }, }, }, + isFirstExistenceFetch: false, }; } @@ -304,6 +305,7 @@ describe('IndexPattern Data Source suggestions', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: { 1: { id: '1', @@ -508,6 +510,7 @@ describe('IndexPattern Data Source suggestions', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: { 1: { id: '1', @@ -1046,6 +1049,7 @@ describe('IndexPattern Data Source suggestions', () => { it('returns no suggestions if there are no columns', () => { expect( getDatasourceSuggestionsFromCurrentState({ + isFirstExistenceFetch: false, indexPatternRefs: [], existingFields: {}, indexPatterns: expectedIndexPatterns, @@ -1351,6 +1355,7 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -1470,6 +1475,7 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -1523,6 +1529,7 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -1553,6 +1560,7 @@ describe('IndexPattern Data Source suggestions', () => { existingFields: {}, currentIndexPatternId: '1', indexPatterns: expectedIndexPatterns, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 9cbd624b42d3e..f9a74ee477d57 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -22,6 +22,7 @@ const initialState: IndexPatternPrivateState = { ], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index e8c8c5762bb83..5776691fbcc7f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/public'; +import { HttpHandler, SavedObjectsClientContract } from 'kibana/public'; import _ from 'lodash'; import { loadInitialState, @@ -429,6 +429,7 @@ describe('loader', () => { indexPatterns: {}, existingFields: {}, layers: {}, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -463,6 +464,7 @@ describe('loader', () => { existingFields: {}, indexPatterns: {}, layers: {}, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -520,6 +522,7 @@ describe('loader', () => { indexPatternId: 'a', }, }, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'a' }); @@ -588,6 +591,7 @@ describe('loader', () => { indexPatternId: 'a', }, }, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -625,7 +629,7 @@ describe('loader', () => { it('should call once for each index pattern', async () => { const setState = jest.fn(); - const fetchJson = jest.fn((path: string) => { + const fetchJson = (jest.fn((path: string) => { const indexPatternTitle = _.last(path.split('/')); return { indexPatternTitle, @@ -633,15 +637,17 @@ describe('loader', () => { (fieldName) => `${indexPatternTitle}_${fieldName}` ), }; - }); + }) as unknown) as HttpHandler; await syncExistingFields({ dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fetchJson: fetchJson as any, + fetchJson, indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], setState, dslQuery, + showNoDataPopover: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, }); expect(fetchJson).toHaveBeenCalledTimes(3); @@ -655,6 +661,7 @@ describe('loader', () => { expect(newState).toEqual({ foo: 'bar', + isFirstExistenceFetch: false, existingFields: { a: { a_field_1: true, a_field_2: true }, b: { b_field_1: true, b_field_2: true }, @@ -662,5 +669,38 @@ describe('loader', () => { }, }); }); + + it('should call showNoDataPopover callback if current index pattern returns no fields', async () => { + const setState = jest.fn(); + const showNoDataPopover = jest.fn(); + const fetchJson = (jest.fn((path: string) => { + const indexPatternTitle = _.last(path.split('/')); + return { + indexPatternTitle, + existingFieldNames: + indexPatternTitle === 'a' + ? ['field_1', 'field_2'].map((fieldName) => `${indexPatternTitle}_${fieldName}`) + : [], + }; + }) as unknown) as HttpHandler; + + const args = { + dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, + fetchJson, + indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + setState, + dslQuery, + showNoDataPopover: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, + }; + + await syncExistingFields(args); + + expect(showNoDataPopover).not.toHaveBeenCalled(); + + await syncExistingFields({ ...args, isFirstExistenceFetch: true }); + expect(showNoDataPopover).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 6c57988dfc7b6..101f536993365 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -119,6 +119,7 @@ export async function loadInitialState({ indexPatternRefs, indexPatterns, existingFields: {}, + isFirstExistenceFetch: true, }; } @@ -128,6 +129,7 @@ export async function loadInitialState({ indexPatterns, layers: {}, existingFields: {}, + isFirstExistenceFetch: true, }; } @@ -238,13 +240,19 @@ export async function syncExistingFields({ dateRange, fetchJson, setState, + isFirstExistenceFetch, + currentIndexPatternTitle, dslQuery, + showNoDataPopover, }: { dateRange: DateRange; indexPatterns: Array<{ id: string; timeFieldName?: string | null }>; fetchJson: HttpSetup['post']; setState: SetState; + isFirstExistenceFetch: boolean; + currentIndexPatternTitle: string; dslQuery: object; + showNoDataPopover: () => void; }) { const emptinessInfo = await Promise.all( indexPatterns.map((pattern) => { @@ -264,8 +272,18 @@ export async function syncExistingFields({ }) ); + if (isFirstExistenceFetch) { + const fieldsCurrentIndexPattern = emptinessInfo.find( + (info) => info.indexPatternTitle === currentIndexPatternTitle + ); + if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { + showNoDataPopover(); + } + } + setState((state) => ({ ...state, + isFirstExistenceFetch: false, existingFields: emptinessInfo.reduce((acc, info) => { acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames); return acc; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index d0c7af42114e3..1a094a36f68e3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -51,6 +51,7 @@ describe('date_histogram', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: { 1: { id: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index 1e1d83a0a5c4c..d7f00e185a5bb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -34,6 +34,7 @@ describe('terms', () => { indexPatterns: {}, existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index a73f6e13d94c5..1a37e5e4cf6a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -147,6 +147,7 @@ describe('getOperationTypesForField', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 65a2401fd689a..d778749ef3940 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -42,6 +42,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -95,6 +96,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -145,6 +147,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -185,6 +188,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -218,6 +222,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -279,6 +284,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -331,6 +337,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -410,6 +417,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 35a82d8774130..b7beb67196add 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -51,6 +51,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { * indexPatternId -> fieldName -> boolean */ existingFields: Record>; + isFirstExistenceFetch: boolean; }; export interface IndexPatternRef { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d451e312446bd..c7bda65cd1327 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -47,6 +47,7 @@ export interface EditorFrameProps { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; }) => void; + showNoDataPopover: () => void; } export interface EditorFrameInstance { mount: (element: Element, props: EditorFrameProps) => void; @@ -186,6 +187,7 @@ export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; setState: StateSetter; + showNoDataPopover: () => void; core: Pick; query: Query; dateRange: DateRange; diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index 00d9208772798..b980116c581da 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'timePicker']); const browser = getService('browser'); const filterBar = getService('filterBar'); const appsMenu = getService('appsMenu'); @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should carry over time range and pinned filters to discover', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); - await PageObjects.timePicker.setAbsoluteRange( + await PageObjects.lens.goToTimeRange( 'Sep 06, 2015 @ 06:31:44.000', 'Sep 18, 2025 @ 06:31:44.000' ); @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should remember time range and pinned filters from discover', async () => { - await PageObjects.timePicker.setAbsoluteRange( + await PageObjects.lens.goToTimeRange( 'Sep 07, 2015 @ 06:31:44.000', 'Sep 19, 2025 @ 06:31:44.000' ); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index bae11e1ea8a90..ce621d4471d0f 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -38,10 +38,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Move the date filter to the specified time range, defaults to * a range that has data in our dataset. */ - goToTimeRange(fromTime?: string, toTime?: string) { + async goToTimeRange(fromTime?: string, toTime?: string) { + await PageObjects.timePicker.ensureHiddenNoDataPopover(); fromTime = fromTime || PageObjects.timePicker.defaultStartTime; toTime = toTime || PageObjects.timePicker.defaultEndTime; - return PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }, /** From 22a41c51e53f851bc67e5e4f63e393a838f6f8aa Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 1 Jul 2020 10:54:22 +0200 Subject: [PATCH 09/29] [Discover] Deangularization context error message refactoring (#70090) Co-authored-by: Elastic Machine --- .../application/angular/context_app.html | 42 ++---------- .../context_error_message.test.tsx | 54 ++++++++++++++++ .../context_error_message.tsx | 64 +++++++++++++++++++ .../context_error_message_directive.ts | 26 ++++++++ .../components/context_error_message/index.ts | 21 ++++++ .../discover/public/get_inner_angular.ts | 2 + 6 files changed, 171 insertions(+), 38 deletions(-) create mode 100644 src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx create mode 100644 src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx create mode 100644 src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts create mode 100644 src/plugins/discover/public/application/components/context_error_message/index.ts diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 9c37fd3bfc5be..6adcaeeae94f5 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -12,44 +12,10 @@ -
-
-
- - -
- -
-
-
-
- -
-
-
-
+ +
{ + component = mountWithIntl(); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); + }); + + it('ContextErrorMessage does not render on success loading', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); + }); + + it('ContextErrorMessage renders just the title if the reason is not specifically handled', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); + expect(findTestSubject(component, 'contextErrorMessageBody').text()).toBe(''); + }); + + it('ContextErrorMessage renders the reason for unknown errors', () => { + component = mountWithIntl( + + ); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); + expect(findTestSubject(component, 'contextErrorMessageBody').length).toBe(1); + }); +}); diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx new file mode 100644 index 0000000000000..f73496c2eeada --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +// @ts-ignore +import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; + +export interface ContextErrorMessageProps { + /** + * the status of the loading action + */ + status: string; + /** + * the reason of the error + */ + reason?: string; +} + +export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps) { + if (status !== LOADING_STATUS.FAILED) { + return null; + } + return ( + + + } + color="danger" + iconType="alert" + data-test-subj="contextErrorMessageTitle" + > + + {reason === FAILURE_REASONS.UNKNOWN && ( + + )} + + + + ); +} diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts b/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts new file mode 100644 index 0000000000000..925d560761a84 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts @@ -0,0 +1,26 @@ +/* + * 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 { ContextErrorMessage } from './context_error_message'; + +export function createContextErrorMessageDirective(reactDirective: any) { + return reactDirective(ContextErrorMessage, [ + ['status', { watchDepth: 'reference' }], + ['reason', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/context_error_message/index.ts b/src/plugins/discover/public/application/components/context_error_message/index.ts new file mode 100644 index 0000000000000..f20f2ccf8afa0 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { ContextErrorMessage } from './context_error_message'; +export { createContextErrorMessageDirective } from './context_error_message_directive'; diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 05513eef93624..0b3c2fad8d45b 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -61,6 +61,7 @@ import { createDiscoverSidebarDirective } from './application/components/sidebar import { createHitsCounterDirective } from '././application/components/hits_counter'; import { createLoadingSpinnerDirective } from '././application/components/loading_spinner/loading_spinner'; import { createTimechartHeaderDirective } from './application/components/timechart_header'; +import { createContextErrorMessageDirective } from './application/components/context_error_message'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createSkipBottomButtonDirective } from './application/components/skip_bottom_button'; @@ -160,6 +161,7 @@ export function initializeInnerAngularModule( .directive('hitsCounter', createHitsCounterDirective) .directive('loadingSpinner', createLoadingSpinnerDirective) .directive('timechartHeader', createTimechartHeaderDirective) + .directive('contextErrorMessage', createContextErrorMessageDirective) .service('debounce', ['$timeout', DebounceProviderTimeout]); } From 5c98b11d73c926733df0ab01c9fb44f6254734d0 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 1 Jul 2020 10:56:05 +0200 Subject: [PATCH 10/29] expressions indexPattern function (#70315) --- .../index_patterns/expressions/index.ts | 20 ++++++ .../expressions/load_index_pattern.test.ts | 39 ++++++++++++ .../expressions/load_index_pattern.ts | 62 +++++++++++++++++++ src/plugins/data/public/plugin.ts | 2 + 4 files changed, 123 insertions(+) create mode 100644 src/plugins/data/public/index_patterns/expressions/index.ts create mode 100644 src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts create mode 100644 src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts diff --git a/src/plugins/data/public/index_patterns/expressions/index.ts b/src/plugins/data/public/index_patterns/expressions/index.ts new file mode 100644 index 0000000000000..fa37e3b216ac9 --- /dev/null +++ b/src/plugins/data/public/index_patterns/expressions/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './load_index_pattern'; diff --git a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts new file mode 100644 index 0000000000000..378ceb376f5f1 --- /dev/null +++ b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { indexPatternLoad } from './load_index_pattern'; + +jest.mock('../../services', () => ({ + getIndexPatterns: () => ({ + get: (id: string) => ({ + toSpec: () => ({ + title: 'value', + }), + }), + }), +})); + +describe('indexPattern expression function', () => { + test('returns serialized index pattern', async () => { + const indexPatternDefinition = indexPatternLoad(); + const result = await indexPatternDefinition.fn(null, { id: '1' }, {} as any); + expect(result.type).toEqual('index_pattern'); + expect(result.value.title).toEqual('value'); + }); +}); diff --git a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts new file mode 100644 index 0000000000000..901d6aac7fbff --- /dev/null +++ b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts @@ -0,0 +1,62 @@ +/* + * 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 { ExpressionFunctionDefinition } from '../../../../../plugins/expressions/public'; +import { getIndexPatterns } from '../../services'; +import { IndexPatternSpec } from '../../../common/index_patterns'; + +const name = 'indexPatternLoad'; + +type Input = null; +type Output = Promise<{ type: 'index_pattern'; value: IndexPatternSpec }>; + +interface Arguments { + id: string; +} + +export const indexPatternLoad = (): ExpressionFunctionDefinition< + typeof name, + Input, + Arguments, + Output +> => ({ + name, + type: 'index_pattern', + inputTypes: ['null'], + help: i18n.translate('data.functions.indexPatternLoad.help', { + defaultMessage: 'Loads an index pattern', + }), + args: { + id: { + types: ['string'], + required: true, + help: i18n.translate('data.functions.indexPatternLoad.id.help', { + defaultMessage: 'index pattern id to load', + }), + }, + }, + async fn(input, args) { + const indexPatterns = getIndexPatterns(); + + const indexPattern = await indexPatterns.get(args.id); + + return { type: 'index_pattern', value: indexPattern.toSpec() }; + }, +}); diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index d5929cb9cd564..ec71794fde87d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -82,6 +82,7 @@ import { ValueClickActionContext, } from './actions/value_click_action'; import { SavedObjectsClientPublicToCommon } from './index_patterns'; +import { indexPatternLoad } from './index_patterns/expressions/load_index_pattern'; declare module '../../ui_actions/public' { export interface ActionContextMapping { @@ -126,6 +127,7 @@ export class DataPublicPlugin implements Plugin Date: Wed, 1 Jul 2020 04:06:56 -0500 Subject: [PATCH 11/29] Initial work on uptime homepage API (#70135) Co-authored-by: Shahzad --- .../common/runtime_types/ping/histogram.ts | 1 + x-pack/plugins/uptime/kibana.json | 2 +- x-pack/plugins/uptime/public/apps/plugin.ts | 13 ++++ .../public/apps/uptime_overview_fetcher.ts | 62 +++++++++++++++++++ .../plugins/uptime/public/state/api/ping.ts | 2 + .../__tests__/get_ping_histogram.test.ts | 3 +- .../server/lib/requests/get_ping_histogram.ts | 29 ++++++--- .../rest_api/pings/get_ping_histogram.ts | 4 +- 8 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts index 209770a19f4aa..47e4dd52299b1 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts @@ -21,6 +21,7 @@ export interface GetPingHistogramParams { dateEnd: string; filters?: string; monitorId?: string; + bucketSize?: string; } export interface HistogramResult { diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 5fbd6129fd18f..152839836ad99 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["capabilities", "data", "home"], + "optionalPlugins": ["capabilities", "data", "home", "observability"], "requiredPlugins": [ "alerts", "embeddable", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 26810a9b1cda3..9af4dea9dbb44 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -27,10 +27,14 @@ import { } from '../../../../../src/plugins/data/public'; import { alertTypeInitializers } from '../lib/alert_types'; import { kibanaService } from '../state/kibana_service'; +import { fetchIndexStatus } from '../state/api'; +import { ObservabilityPluginSetup } from '../../../observability/public'; +import { fetchUptimeOverviewData } from './uptime_overview_fetcher'; export interface ClientPluginsSetup { data: DataPublicPluginSetup; home: HomePublicPluginSetup; + observability: ObservabilityPluginSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; } @@ -63,6 +67,15 @@ export class UptimePlugin }); } + plugins.observability.dashboard.register({ + appName: 'uptime', + hasData: async () => { + const status = await fetchIndexStatus(); + return status.docCount > 0; + }, + fetchData: fetchUptimeOverviewData, + }); + core.application.register({ appRoute: '/app/uptime#/', id: PLUGIN.ID, diff --git a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts new file mode 100644 index 0000000000000..8467714e9661e --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchPingHistogram, fetchSnapshotCount } from '../state/api'; +import { UptimeFetchDataResponse } from '../../../observability/public/typings/fetch_data_response'; + +export async function fetchUptimeOverviewData({ + startTime, + endTime, + bucketSize, +}: { + startTime: string; + endTime: string; + bucketSize: string; +}) { + const snapshot = await fetchSnapshotCount({ + dateRangeStart: startTime, + dateRangeEnd: endTime, + }); + + const pings = await fetchPingHistogram({ dateStart: startTime, dateEnd: endTime, bucketSize }); + + const response: UptimeFetchDataResponse = { + title: 'Uptime', + appLink: '/app/uptime#/', + stats: { + monitors: { + type: 'number', + label: 'Monitors', + value: snapshot.total, + }, + up: { + type: 'number', + label: 'Up', + value: snapshot.up, + }, + down: { + type: 'number', + label: 'Down', + value: snapshot.down, + }, + }, + series: { + up: { + label: 'Up', + coordinates: pings.histogram.map((p) => { + return { x: p.x!, y: p.upCount || 0 }; + }), + }, + down: { + label: 'Down', + coordinates: pings.histogram.map((p) => { + return { x: p.x!, y: p.downCount || 0 }; + }), + }, + }, + }; + return response; +} diff --git a/x-pack/plugins/uptime/public/state/api/ping.ts b/x-pack/plugins/uptime/public/state/api/ping.ts index a2937c9c794dd..2d6a69064f277 100644 --- a/x-pack/plugins/uptime/public/state/api/ping.ts +++ b/x-pack/plugins/uptime/public/state/api/ping.ts @@ -25,12 +25,14 @@ export const fetchPingHistogram: APIFn dateStart, dateEnd, filters, + bucketSize, }) => { const queryParams = { dateStart, dateEnd, monitorId, filters, + bucketSize, }; return await apiService.get(API_URLS.PING_HISTOGRAM, queryParams); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 9042186145eb7..11c7511dec370 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -62,7 +62,6 @@ describe('getPingHistogram', () => { dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', - filters: null, }); expect(mockEsClient).toHaveBeenCalledTimes(1); @@ -81,7 +80,7 @@ describe('getPingHistogram', () => { dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', - filters: null, + filters: '', }); expect(mockEsClient).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 863eff82c360e..a74b55c24e227 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -15,15 +15,17 @@ export interface GetPingHistogramParams { /** @member dateRangeEnd timestamp bounds */ to: string; /** @member filters user-defined filters */ - filters?: string | null; + filters?: string; /** @member monitorId optional limit to monitorId */ - monitorId?: string | null; + monitorId?: string; + + bucketSize?: string; } export const getPingHistogram: UMElasticsearchQueryFn< GetPingHistogramParams, HistogramResult -> = async ({ callES, dynamicSettings, from, to, filters, monitorId }) => { +> = async ({ callES, dynamicSettings, from, to, filters, monitorId, bucketSize }) => { const boolFilters = filters ? JSON.parse(filters) : null; const additionalFilters = []; if (monitorId) { @@ -34,6 +36,22 @@ export const getPingHistogram: UMElasticsearchQueryFn< } const filter = getFilterClause(from, to, additionalFilters); + const seriesHistogram: any = {}; + + if (bucketSize) { + seriesHistogram.date_histogram = { + field: '@timestamp', + fixed_interval: bucketSize, + missing: 0, + }; + } else { + seriesHistogram.auto_date_histogram = { + field: '@timestamp', + buckets: QUERY.DEFAULT_BUCKET_COUNT, + missing: 0, + }; + } + const params = { index: dynamicSettings.heartbeatIndices, body: { @@ -45,10 +63,7 @@ export const getPingHistogram: UMElasticsearchQueryFn< size: 0, aggs: { timeseries: { - auto_date_histogram: { - field: '@timestamp', - buckets: QUERY.DEFAULT_BUCKET_COUNT, - }, + ...seriesHistogram, aggs: { down: { filter: { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index a589997889069..4ac50d0e78c4c 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -18,10 +18,11 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe dateEnd: schema.string(), monitorId: schema.maybe(schema.string()), filters: schema.maybe(schema.string()), + bucketSize: schema.maybe(schema.string()), }), }, handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { - const { dateStart, dateEnd, monitorId, filters } = request.query; + const { dateStart, dateEnd, monitorId, filters, bucketSize } = request.query; const result = await libs.requests.getPingHistogram({ callES, @@ -30,6 +31,7 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe to: dateEnd, monitorId, filters, + bucketSize, }); return response.ok({ From 8a6a55097da187f07ca8c6f298f8586b4f1fe435 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 1 Jul 2020 03:16:23 -0700 Subject: [PATCH 12/29] Enable "Explore underlying data" actions for Lens visualizations (#70047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 rename folder to "explore_data" * style: 💄 check for "share" plugin in more semantic way "explore data" actions use Discover URL generator, which is registered in "share" plugin, which is optional plugin, so we check for its existance, because otherwise URL generator is not available. * refactor: 💡 move KibanaURL to a separate file * feat: 🎸 add "Explore underlying data" in-chart action * fix: 🐛 fix imports after refactor * feat: 🎸 add start.filtersFromContext to embeddable plugin * feat: 🎸 add type checkers to data plugin * feat: 🎸 better handle empty filters in Discover URL generator * feat: 🎸 implement .getUrl() method of explore data in-chart act * feat: 🎸 add embeddable.filtersAndTimeRangeFromContext() * feat: 🎸 improve getUrl() method of explore data action * test: 💍 update test mock * fix possible stale hashHistory.location in discover * style: 💄 ensureHashHistoryLocation -> syncHistoryLocations * docs: ✏️ update autogenerated docs * test: 💍 add in-chart "Explore underlying data" unit tests * test: 💍 add in-chart "Explore underlying data" functional tests * test: 💍 clean-up custom time range after panel action tests * chore: 🤖 fix embeddable plugin mocks * chore: 🤖 fix another mock * test: 💍 add support for new action to pie chart service * feat: 🎸 enable "Explore underlying data" action for Lens vis * test: 💍 make tests green again * refactor: 💡 rename trigger contexts * chore: 🤖 fix TypeScript errors Co-authored-by: Anton Dosov Co-authored-by: Elastic Machine --- .../create_filters_from_range_select.ts | 4 +-- .../create_filters_from_value_click.test.ts | 4 +-- .../create_filters_from_value_click.ts | 4 +-- .../public/actions/select_range_action.ts | 4 +-- .../data/public/actions/value_click_action.ts | 4 +-- src/plugins/embeddable/public/index.ts | 4 +-- .../public/lib/triggers/triggers.ts | 12 ++++----- src/plugins/ui_actions/public/types.ts | 6 ++--- .../dashboard_hello_world_drilldown/index.tsx | 7 ++--- .../dashboard_to_discover_drilldown/types.ts | 7 ++--- .../dashboard_to_url_drilldown/index.tsx | 7 ++--- .../drilldown.test.tsx | 8 +++--- .../dashboard_to_dashboard_drilldown/types.ts | 8 +++--- .../abstract_explore_data_action.ts | 7 +++-- .../explore_data_chart_action.test.ts | 21 ++++++++++----- .../explore_data/explore_data_chart_action.ts | 8 +++--- .../explore_data_context_menu_action.test.ts | 13 ++++++--- .../explore_data_context_menu_action.ts | 2 +- .../public/actions/explore_data/shared.ts | 27 +++++-------------- 19 files changed, 74 insertions(+), 83 deletions(-) diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts index 409614ca9c380..a0eb49d773f3d 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -22,9 +22,9 @@ import moment from 'moment'; import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; import { getIndexPatterns } from '../../../public/services'; import { deserializeAggConfig } from '../../search/expressions/utils'; -import { RangeSelectTriggerContext } from '../../../../embeddable/public'; +import { RangeSelectContext } from '../../../../embeddable/public'; -export async function createFiltersFromRangeSelectAction(event: RangeSelectTriggerContext['data']) { +export async function createFiltersFromRangeSelectAction(event: RangeSelectContext['data']) { const column: Record = event.table.columns[event.column]; if (!column || !column.meta) { diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index a0e285c20d776..3e38477a908b8 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -27,7 +27,7 @@ import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns } from '../../../public/services'; import { mockDataServices } from '../../../public/search/aggs/test_helpers'; import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; -import { ValueClickTriggerContext } from '../../../../embeddable/public'; +import { ValueClickContext } from '../../../../embeddable/public'; const mockField = { name: 'bytes', @@ -39,7 +39,7 @@ const mockField = { }; describe('createFiltersFromValueClick', () => { - let dataPoints: ValueClickTriggerContext['data']['data']; + let dataPoints: ValueClickContext['data']['data']; beforeEach(() => { dataPoints = [ diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index 2fdd746535519..1974b9f776748 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -21,7 +21,7 @@ import { KibanaDatatable } from '../../../../../plugins/expressions/public'; import { deserializeAggConfig } from '../../search/expressions'; import { esFilters, Filter } from '../../../public'; import { getIndexPatterns } from '../../../public/services'; -import { ValueClickTriggerContext } from '../../../../embeddable/public'; +import { ValueClickContext } from '../../../../embeddable/public'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -114,7 +114,7 @@ const createFilter = async ( export const createFiltersFromValueClickAction = async ({ data, negate, -}: ValueClickTriggerContext['data']) => { +}: ValueClickContext['data']) => { const filters: Filter[] = []; await Promise.all( diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 18853f7e292f6..49766143b5588 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -24,12 +24,12 @@ import { ActionByType, } from '../../../../plugins/ui_actions/public'; import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; -import { RangeSelectTriggerContext } from '../../../embeddable/public'; +import { RangeSelectContext } from '../../../embeddable/public'; import { FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -export type SelectRangeActionContext = RangeSelectTriggerContext; +export type SelectRangeActionContext = RangeSelectContext; async function isCompatible(context: SelectRangeActionContext) { try { diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 5d4f1f5f1d6db..dd74a7ee507f3 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -27,12 +27,12 @@ import { import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; -import { ValueClickTriggerContext } from '../../../embeddable/public'; +import { ValueClickContext } from '../../../embeddable/public'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -export type ValueClickActionContext = ValueClickTriggerContext; +export type ValueClickActionContext = ValueClickContext; async function isCompatible(context: ValueClickActionContext) { try { diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index f19974942c43d..6960550b59d1c 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -48,8 +48,8 @@ export { EmbeddableOutput, EmbeddablePanel, EmbeddableRoot, - ValueClickTriggerContext, - RangeSelectTriggerContext, + ValueClickContext, + RangeSelectContext, ErrorEmbeddable, IContainer, IEmbeddable, diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 5bb96a708b7ac..ccba5cf771088 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -25,7 +25,7 @@ export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface ValueClickTriggerContext { +export interface ValueClickContext { embeddable?: T; data: { data: Array<{ @@ -39,7 +39,7 @@ export interface ValueClickTriggerContext { }; } -export interface RangeSelectTriggerContext { +export interface RangeSelectContext { embeddable?: T; data: { table: KibanaDatatable; @@ -50,16 +50,16 @@ export interface RangeSelectTriggerContext } export type ChartActionContext = - | ValueClickTriggerContext - | RangeSelectTriggerContext; + | ValueClickContext + | RangeSelectContext; export const isValueClickTriggerContext = ( context: ChartActionContext -): context is ValueClickTriggerContext => context.data && 'data' in context.data; +): context is ValueClickContext => context.data && 'data' in context.data; export const isRangeSelectTriggerContext = ( context: ChartActionContext -): context is RangeSelectTriggerContext => context.data && 'range' in context.data; +): context is RangeSelectContext => context.data && 'range' in context.data; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 85c87306cc4f9..9fcd8a32881df 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -22,7 +22,7 @@ import { TriggerInternal } from './triggers/trigger_internal'; import { Filter } from '../../data/public'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; import { IEmbeddable } from '../../embeddable/public'; -import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public'; +import { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; export type TriggerRegistry = Map>; export type ActionRegistry = Map; @@ -37,8 +37,8 @@ export type TriggerContext = BaseContext; export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; - [SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext; - [VALUE_CLICK_TRIGGER]: ValueClickTriggerContext; + [SELECT_RANGE_TRIGGER]: RangeSelectContext; + [VALUE_CLICK_TRIGGER]: ValueClickContext; [APPLY_FILTER_TRIGGER]: { embeddable: IEmbeddable; filters: Filter[]; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx index bfe853241ae1d..2598d66c4976f 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -8,13 +8,10 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; -import { - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; +import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public'; -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = ChartActionContext; export interface Config { name: string; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts index 5dfc250a56d28..d8147827ed473 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; +import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = ChartActionContext; export interface Config { /** diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 5e4ba54864461..037e017097e53 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -8,10 +8,7 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; -import { - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; +import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; function isValidUrl(url: string) { @@ -23,7 +20,7 @@ function isValidUrl(url: string) { } } -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = ChartActionContext; export interface Config { url: string; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 6ce7dccd3a3ec..52b232afa9410 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -22,8 +22,8 @@ import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/da import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators'; import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; import { - RangeSelectTriggerContext, - ValueClickTriggerContext, + RangeSelectContext, + ValueClickContext, } from '../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../plugin'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; @@ -136,8 +136,8 @@ describe('.execute() & getHref', () => { const context = ({ data: { ...(useRangeEvent - ? ({ range: {} } as RangeSelectTriggerContext['data']) - : ({ data: [] } as ValueClickTriggerContext['data'])), + ? ({ range: {} } as RangeSelectContext['data']) + : ({ data: [] } as ValueClickContext['data'])), timeFieldName: 'order_date', }, embeddable: { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 1fbff0a7269e2..6be2e2a77269f 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -5,14 +5,14 @@ */ import { - ValueClickTriggerContext, - RangeSelectTriggerContext, + ValueClickContext, + RangeSelectContext, IEmbeddable, } from '../../../../../../../src/plugins/embeddable/public'; export type ActionContext = - | ValueClickTriggerContext - | RangeSelectTriggerContext; + | ValueClickContext + | RangeSelectContext; export interface Config { dashboardId?: string; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 620cabe652778..59359fb35f544 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -43,14 +43,13 @@ export abstract class AbstractExploreDataAction { if (!embeddable) return false; if (!this.params.start().plugins.discover.urlGenerator) return false; - if (!shared.isVisualizeEmbeddable(embeddable)) return false; - if (!shared.getIndexPattern(embeddable)) return false; + if (!shared.hasExactlyOneIndexPattern(embeddable)) return false; if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; return true; } public async execute(context: Context): Promise { - if (!shared.isVisualizeEmbeddable(context.embeddable)) return; + if (!shared.hasExactlyOneIndexPattern(context.embeddable)) return; const { core } = this.params.start(); const { appName, appPath } = await this.getUrl(context); @@ -63,7 +62,7 @@ export abstract class AbstractExploreDataAction { const { embeddable } = context; - if (!shared.isVisualizeEmbeddable(embeddable)) { + if (!shared.hasExactlyOneIndexPattern(embeddable)) { throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`); } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index a273f0d50e45e..0d22f0a36d418 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -10,8 +10,8 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; import { EmbeddableStart, - RangeSelectTriggerContext, - ValueClickTriggerContext, + RangeSelectContext, + ValueClickContext, ChartActionContext, } from '../../../../../../src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; @@ -85,8 +85,8 @@ const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => { const data: ChartActionContext['data'] = { ...(useRangeEvent - ? ({ range: {} } as RangeSelectTriggerContext['data']) - : ({ data: [] } as ValueClickTriggerContext['data'])), + ? ({ range: {} } as RangeSelectContext['data']) + : ({ data: [] } as ValueClickContext['data'])), timeFieldName: 'order_date', }; @@ -139,9 +139,16 @@ describe('"Explore underlying data" panel action', () => { expect(isCompatible).toBe(false); }); - test('returns false if embeddable is not Visualize embeddable', async () => { - const { action, embeddable, context } = setup(); - (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + test('returns false if embeddable has more than one index pattern', async () => { + const { action, output, context } = setup(); + output.indexPatterns = [ + { + id: 'index-ptr-foo', + }, + { + id: 'index-ptr-bar', + }, + ]; const isCompatible = await action.isCompatible(context); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 359f14959c6a6..658a6bcb3cf4d 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -6,8 +6,8 @@ import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { - ValueClickTriggerContext, - RangeSelectTriggerContext, + ValueClickContext, + RangeSelectContext, } from '../../../../../../src/plugins/embeddable/public'; import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; @@ -15,7 +15,7 @@ import { KibanaURL } from './kibana_url'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; -export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext; +export type ExploreDataChartActionContext = ValueClickContext | RangeSelectContext; export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; @@ -49,7 +49,7 @@ export class ExploreDataChartAction extends AbstractExploreDataAction { expect(isCompatible).toBe(false); }); - test('returns false if embeddable is not Visualize embeddable', async () => { - const { action, embeddable, context } = setup(); - (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + test('returns false if embeddable has more than one index pattern', async () => { + const { action, output, context } = setup(); + output.indexPatterns = [ + { + id: 'index-ptr-foo', + }, + { + id: 'index-ptr-bar', + }, + ]; const isCompatible = await action.isCompatible(context); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts index 6691089f875d8..8b79211a914cc 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -38,7 +38,7 @@ export class ExploreDataContextMenuAction extends AbstractExploreDataAction } => { if (!output || typeof output !== 'object') return false; return Array.isArray((output as any).indexPatterns); }; -export const isVisualizeEmbeddable = ( - embeddable?: IEmbeddable -): embeddable is VisualizeEmbeddableContract => - embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false; - -/** - * @returns Returns empty string if no index pattern ID found. - */ -export const getIndexPattern = (embeddable?: IEmbeddable): string => { - if (!embeddable) return ''; +export const getIndexPatterns = (embeddable?: IEmbeddable): string[] => { + if (!embeddable) return []; const output = embeddable.getOutput(); - if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { - return output.indexPatterns[0].id; - } - - return ''; + return isOutputWithIndexPatterns(output) ? output.indexPatterns.map(({ id }) => id) : []; }; + +export const hasExactlyOneIndexPattern = (embeddable?: IEmbeddable): boolean => + getIndexPatterns(embeddable).length === 1; From e70bc819987f064c3f224adc479808b73a0dd66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 1 Jul 2020 14:05:29 +0200 Subject: [PATCH 13/29] [Logs UI] Avoid CCS-incompatible index name resolution (#70179) This fixes #70048 by avoiding a CCS-incompatible ES API call when determining the existence of log indices. --- .../http_api/log_sources/get_log_source_status.ts | 2 +- .../public/containers/logs/log_source/log_source.ts | 6 ------ .../infra/public/pages/logs/stream/page_content.tsx | 4 ++-- .../infra/public/pages/logs/stream/page_providers.tsx | 4 ++-- .../plugins/infra/server/routes/log_sources/status.ts | 11 +++++------ 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts index ae872cee9aa56..b522d86987283 100644 --- a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts @@ -42,7 +42,7 @@ export type LogIndexField = rt.TypeOf; const logSourceStatusRT = rt.strict({ logIndexFields: rt.array(logIndexFieldRT), - logIndexNames: rt.array(rt.string), + logIndicesExist: rt.boolean, }); export type LogSourceStatus = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 80aab6237518f..b45ea0a042f49 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -78,11 +78,6 @@ export const useLogSource = ({ [sourceId, fetch] ); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [ - sourceStatus, - ]); - const derivedIndexPattern = useMemo( () => ({ fields: sourceStatus?.logIndexFields ?? [], @@ -160,7 +155,6 @@ export const useLogSource = ({ loadSourceFailureMessage, loadSourceConfiguration, loadSourceStatus, - logIndicesExist, sourceConfiguration, sourceId, sourceStatus, diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index 40ac5c74a6836..b2a4ce65ab2b6 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -18,14 +18,14 @@ export const StreamPageContent: React.FunctionComponent = () => { isUninitialized, loadSource, loadSourceFailureMessage, - logIndicesExist, + sourceStatus, } = useLogSourceContext(); if (isLoading || isUninitialized) { return ; } else if (hasFailedLoadingSource) { return ; - } else if (logIndicesExist) { + } else if (sourceStatus?.logIndicesExist) { return ; } else { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx index 428a7d3fdfe4b..82c21f663bc96 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -104,10 +104,10 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => { }; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const { logIndicesExist } = useLogSourceContext(); + const { sourceStatus } = useLogSourceContext(); // The providers assume the source is loaded, so short-circuit them otherwise - if (!logIndicesExist) { + if (!sourceStatus?.logIndicesExist) { return <>{children}; } diff --git a/x-pack/plugins/infra/server/routes/log_sources/status.ts b/x-pack/plugins/infra/server/routes/log_sources/status.ts index cdd053d2bb10a..4cd85ecfe23c1 100644 --- a/x-pack/plugins/infra/server/routes/log_sources/status.ts +++ b/x-pack/plugins/infra/server/routes/log_sources/status.ts @@ -31,17 +31,16 @@ export const initLogSourceStatusRoutes = ({ const { sourceId } = request.params; try { - const logIndexNames = await sourceStatus.getLogIndexNames(requestContext, sourceId); - const logIndexFields = - logIndexNames.length > 0 - ? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS) - : []; + const logIndicesExist = await sourceStatus.hasLogIndices(requestContext, sourceId); + const logIndexFields = logIndicesExist + ? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS) + : []; return response.ok({ body: getLogSourceStatusSuccessResponsePayloadRT.encode({ data: { + logIndicesExist, logIndexFields, - logIndexNames, }, }), }); From 518e88cf287ee8fdfefc51ae5040765371d8065b Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 1 Jul 2020 14:20:02 +0200 Subject: [PATCH 14/29] update (#70424) --- x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts index 8467714e9661e..bede391537ec5 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts +++ b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts @@ -5,7 +5,7 @@ */ import { fetchPingHistogram, fetchSnapshotCount } from '../state/api'; -import { UptimeFetchDataResponse } from '../../../observability/public/typings/fetch_data_response'; +import { UptimeFetchDataResponse } from '../../../observability/public'; export async function fetchUptimeOverviewData({ startTime, From 1d1051b1e988e80599f0375d8efad39113537a57 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Wed, 1 Jul 2020 10:04:21 -0400 Subject: [PATCH 15/29] Changes observability plugin codeowner (#70439) The observability plugin is now code-owned by a new observability-ui GH team to avoid pinging 4 separate teams for mandatory reviews when that plugin changes. --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a94180e60e05e..bec0a0a33bad2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -84,7 +84,7 @@ /x-pack/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/ingest_manager/ @elastic/ingest-management /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management -/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management +/x-pack/plugins/observability/ @elastic/observability-ui /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime From 7ed1fe05d7935689871939ff68acd34fe5c02e3f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 1 Jul 2020 08:05:59 -0600 Subject: [PATCH 16/29] Move logger configuration integration test to jest (#70378) --- .../logging/integration_tests/logging.test.ts | 161 ++++++++++++++++++ .../plugins/core_logging/kibana.json | 7 - .../plugins/core_logging/server/.gitignore | 1 - .../plugins/core_logging/server/index.ts | 23 --- .../plugins/core_logging/server/plugin.ts | 118 ------------- .../plugins/core_logging/tsconfig.json | 13 -- .../test_suites/core_plugins/index.ts | 1 - .../test_suites/core_plugins/logging.ts | 146 ---------------- 8 files changed, 161 insertions(+), 309 deletions(-) delete mode 100644 test/plugin_functional/plugins/core_logging/kibana.json delete mode 100644 test/plugin_functional/plugins/core_logging/server/.gitignore delete mode 100644 test/plugin_functional/plugins/core_logging/server/index.ts delete mode 100644 test/plugin_functional/plugins/core_logging/server/plugin.ts delete mode 100644 test/plugin_functional/plugins/core_logging/tsconfig.json delete mode 100644 test/plugin_functional/test_suites/core_plugins/logging.ts diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index b88f5ba2c2b60..a80939a25ae65 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -18,6 +18,9 @@ */ import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { InternalCoreSetup } from '../../internal_types'; +import { LoggerContextConfigInput } from '../logging_config'; +import { Subject } from 'rxjs'; function createRoot() { return kbnTestServer.createRoot({ @@ -111,4 +114,162 @@ describe('logging service', () => { expect(mockConsoleLog).toHaveBeenCalledTimes(0); }); }); + + describe('custom context configuration', () => { + const CUSTOM_LOGGING_CONFIG: LoggerContextConfigInput = { + appenders: { + customJsonConsole: { + kind: 'console', + layout: { + kind: 'json', + }, + }, + customPatternConsole: { + kind: 'console', + layout: { + kind: 'pattern', + pattern: 'CUSTOM - PATTERN [%logger][%level] %message', + }, + }, + }, + + loggers: [ + { context: 'debug_json', appenders: ['customJsonConsole'], level: 'debug' }, + { context: 'debug_pattern', appenders: ['customPatternConsole'], level: 'debug' }, + { context: 'info_json', appenders: ['customJsonConsole'], level: 'info' }, + { context: 'info_pattern', appenders: ['customPatternConsole'], level: 'info' }, + { + context: 'all', + appenders: ['customJsonConsole', 'customPatternConsole'], + level: 'debug', + }, + ], + }; + + let root: ReturnType; + let setup: InternalCoreSetup; + let mockConsoleLog: jest.SpyInstance; + const loggingConfig$ = new Subject(); + const setContextConfig = (enable: boolean) => + enable ? loggingConfig$.next(CUSTOM_LOGGING_CONFIG) : loggingConfig$.next({}); + beforeAll(async () => { + mockConsoleLog = jest.spyOn(global.console, 'log'); + root = kbnTestServer.createRoot(); + + setup = await root.setup(); + setup.logging.configure(['plugins', 'myplugin'], loggingConfig$); + }, 30000); + + beforeEach(() => { + mockConsoleLog.mockClear(); + }); + + afterAll(async () => { + mockConsoleLog.mockRestore(); + await root.shutdown(); + }); + + it('does not write to custom appenders when not configured', async () => { + const logger = root.logger.get('plugins.myplugin.debug_pattern'); + setContextConfig(false); + logger.info('log1'); + setContextConfig(true); + logger.debug('log2'); + logger.info('log3'); + setContextConfig(false); + logger.info('log4'); + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][DEBUG] log2' + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][INFO ] log3' + ); + }); + + it('writes debug_json context to custom JSON appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.debug_json'); + logger.debug('log1'); + logger.info('log2'); + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + + const [firstCall, secondCall] = mockConsoleLog.mock.calls.map(([jsonString]) => + JSON.parse(jsonString) + ); + expect(firstCall).toMatchObject({ + level: 'DEBUG', + context: 'plugins.myplugin.debug_json', + message: 'log1', + }); + expect(secondCall).toMatchObject({ + level: 'INFO', + context: 'plugins.myplugin.debug_json', + message: 'log2', + }); + }); + + it('writes info_json context to custom JSON appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.info_json'); + logger.debug('i should not be logged!'); + logger.info('log2'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({ + level: 'INFO', + context: 'plugins.myplugin.info_json', + message: 'log2', + }); + }); + + it('writes debug_pattern context to custom pattern appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.debug_pattern'); + logger.debug('log1'); + logger.info('log2'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][DEBUG] log1' + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][INFO ] log2' + ); + }); + + it('writes info_pattern context to custom pattern appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.info_pattern'); + logger.debug('i should not be logged!'); + logger.info('log2'); + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.info_pattern][INFO ] log2' + ); + }); + + it('writes all context to both appenders', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.all'); + logger.debug('log1'); + logger.info('log2'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(4); + const logs = mockConsoleLog.mock.calls.map(([jsonString]) => jsonString); + + expect(JSON.parse(logs[0])).toMatchObject({ + level: 'DEBUG', + context: 'plugins.myplugin.all', + message: 'log1', + }); + expect(logs[1]).toEqual('CUSTOM - PATTERN [plugins.myplugin.all][DEBUG] log1'); + expect(JSON.parse(logs[2])).toMatchObject({ + level: 'INFO', + context: 'plugins.myplugin.all', + message: 'log2', + }); + expect(logs[3]).toEqual('CUSTOM - PATTERN [plugins.myplugin.all][INFO ] log2'); + }); + }); }); diff --git a/test/plugin_functional/plugins/core_logging/kibana.json b/test/plugin_functional/plugins/core_logging/kibana.json deleted file mode 100644 index 3289c2c627b9a..0000000000000 --- a/test/plugin_functional/plugins/core_logging/kibana.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "core_logging", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["core_logging"], - "server": true -} diff --git a/test/plugin_functional/plugins/core_logging/server/.gitignore b/test/plugin_functional/plugins/core_logging/server/.gitignore deleted file mode 100644 index 9a3d281179193..0000000000000 --- a/test/plugin_functional/plugins/core_logging/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*debug.log diff --git a/test/plugin_functional/plugins/core_logging/server/index.ts b/test/plugin_functional/plugins/core_logging/server/index.ts deleted file mode 100644 index ca1d9da95b495..0000000000000 --- a/test/plugin_functional/plugins/core_logging/server/index.ts +++ /dev/null @@ -1,23 +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 type { PluginInitializerContext } from '../../../../../src/core/server'; -import { CoreLoggingPlugin } from './plugin'; - -export const plugin = (init: PluginInitializerContext) => new CoreLoggingPlugin(init); diff --git a/test/plugin_functional/plugins/core_logging/server/plugin.ts b/test/plugin_functional/plugins/core_logging/server/plugin.ts deleted file mode 100644 index a7820a0f67525..0000000000000 --- a/test/plugin_functional/plugins/core_logging/server/plugin.ts +++ /dev/null @@ -1,118 +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 { resolve } from 'path'; -import { Subject } from 'rxjs'; -import { schema } from '@kbn/config-schema'; -import type { - PluginInitializerContext, - Plugin, - CoreSetup, - LoggerContextConfigInput, - Logger, -} from '../../../../../src/core/server'; - -const CUSTOM_LOGGING_CONFIG: LoggerContextConfigInput = { - appenders: { - customJsonFile: { - kind: 'file', - path: resolve(__dirname, 'json_debug.log'), // use 'debug.log' suffix so file watcher does not restart server - layout: { - kind: 'json', - }, - }, - customPatternFile: { - kind: 'file', - path: resolve(__dirname, 'pattern_debug.log'), - layout: { - kind: 'pattern', - pattern: 'CUSTOM - PATTERN [%logger][%level] %message', - }, - }, - }, - - loggers: [ - { context: 'debug_json', appenders: ['customJsonFile'], level: 'debug' }, - { context: 'debug_pattern', appenders: ['customPatternFile'], level: 'debug' }, - { context: 'info_json', appenders: ['customJsonFile'], level: 'info' }, - { context: 'info_pattern', appenders: ['customPatternFile'], level: 'info' }, - { context: 'all', appenders: ['customJsonFile', 'customPatternFile'], level: 'debug' }, - ], -}; - -export class CoreLoggingPlugin implements Plugin { - private readonly logger: Logger; - - constructor(init: PluginInitializerContext) { - this.logger = init.logger.get(); - } - - public setup(core: CoreSetup) { - const loggingConfig$ = new Subject(); - core.logging.configure(loggingConfig$); - - const router = core.http.createRouter(); - - // Expose a route that allows our test suite to write logs as this plugin - router.post( - { - path: '/internal/core-logging/write-log', - validate: { - body: schema.object({ - level: schema.oneOf([schema.literal('debug'), schema.literal('info')]), - message: schema.string(), - context: schema.arrayOf(schema.string()), - }), - }, - }, - (ctx, req, res) => { - const { level, message, context } = req.body; - const logger = this.logger.get(...context); - - if (level === 'debug') { - logger.debug(message); - } else if (level === 'info') { - logger.info(message); - } - - return res.ok(); - } - ); - - // Expose a route to toggle on and off the custom config - router.post( - { - path: '/internal/core-logging/update-config', - validate: { body: schema.object({ enableCustomConfig: schema.boolean() }) }, - }, - (ctx, req, res) => { - if (req.body.enableCustomConfig) { - loggingConfig$.next(CUSTOM_LOGGING_CONFIG); - } else { - loggingConfig$.next({}); - } - - return res.ok({ body: `Updated config: ${req.body.enableCustomConfig}` }); - } - ); - } - - public start() {} - public stop() {} -} diff --git a/test/plugin_functional/plugins/core_logging/tsconfig.json b/test/plugin_functional/plugins/core_logging/tsconfig.json deleted file mode 100644 index 7389eb6ce159b..0000000000000 --- a/test/plugin_functional/plugins/core_logging/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true - }, - "include": [ - "index.ts", - "server/**/*.ts", - "../../../../typings/**/*", - ], - "exclude": [] -} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 8f7c2267d34b4..8f54ec6c0f4cd 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -30,6 +30,5 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./application_leave_confirm')); loadTestFile(require.resolve('./application_status')); loadTestFile(require.resolve('./rendering')); - loadTestFile(require.resolve('./logging')); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/logging.ts b/test/plugin_functional/test_suites/core_plugins/logging.ts deleted file mode 100644 index 9fdaa6ce834ea..0000000000000 --- a/test/plugin_functional/test_suites/core_plugins/logging.ts +++ /dev/null @@ -1,146 +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 { resolve } from 'path'; -import fs from 'fs'; -import expect from '@kbn/expect'; -import { PluginFunctionalProviderContext } from '../../services'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: PluginFunctionalProviderContext) { - const supertest = getService('supertest'); - - describe('plugin logging', function describeIndexTests() { - const LOG_FILE_DIRECTORY = resolve(__dirname, '..', '..', 'plugins', 'core_logging', 'server'); - const JSON_FILE_PATH = resolve(LOG_FILE_DIRECTORY, 'json_debug.log'); - const PATTERN_FILE_PATH = resolve(LOG_FILE_DIRECTORY, 'pattern_debug.log'); - - beforeEach(async () => { - // "touch" each file to ensure it exists and is empty before each test - await fs.promises.writeFile(JSON_FILE_PATH, ''); - await fs.promises.writeFile(PATTERN_FILE_PATH, ''); - }); - - async function readLines(path: string) { - const contents = await fs.promises.readFile(path, { encoding: 'utf8' }); - return contents.trim().split('\n'); - } - - async function readJsonLines() { - return (await readLines(JSON_FILE_PATH)) - .filter((line) => line.length > 0) - .map((line) => JSON.parse(line)) - .map(({ level, message, context }) => ({ level, message, context })); - } - - function writeLog(context: string[], level: string, message: string) { - return supertest - .post('/internal/core-logging/write-log') - .set('kbn-xsrf', 'anything') - .send({ context, level, message }) - .expect(200); - } - - function setContextConfig(enable: boolean) { - return supertest - .post('/internal/core-logging/update-config') - .set('kbn-xsrf', 'anything') - .send({ enableCustomConfig: enable }) - .expect(200); - } - - it('does not write to custom appenders when not configured', async () => { - await setContextConfig(false); - await writeLog(['debug_json'], 'info', 'i go to the default appender!'); - expect(await readJsonLines()).to.eql([]); - }); - - it('writes debug_json context to custom JSON appender', async () => { - await setContextConfig(true); - await writeLog(['debug_json'], 'debug', 'log1'); - await writeLog(['debug_json'], 'info', 'log2'); - expect(await readJsonLines()).to.eql([ - { - level: 'DEBUG', - context: 'plugins.core_logging.debug_json', - message: 'log1', - }, - { - level: 'INFO', - context: 'plugins.core_logging.debug_json', - message: 'log2', - }, - ]); - }); - - it('writes info_json context to custom JSON appender', async () => { - await setContextConfig(true); - await writeLog(['info_json'], 'debug', 'i should not be logged!'); - await writeLog(['info_json'], 'info', 'log2'); - expect(await readJsonLines()).to.eql([ - { - level: 'INFO', - context: 'plugins.core_logging.info_json', - message: 'log2', - }, - ]); - }); - - it('writes debug_pattern context to custom pattern appender', async () => { - await setContextConfig(true); - await writeLog(['debug_pattern'], 'debug', 'log1'); - await writeLog(['debug_pattern'], 'info', 'log2'); - expect(await readLines(PATTERN_FILE_PATH)).to.eql([ - 'CUSTOM - PATTERN [plugins.core_logging.debug_pattern][DEBUG] log1', - 'CUSTOM - PATTERN [plugins.core_logging.debug_pattern][INFO ] log2', - ]); - }); - - it('writes info_pattern context to custom pattern appender', async () => { - await setContextConfig(true); - await writeLog(['info_pattern'], 'debug', 'i should not be logged!'); - await writeLog(['info_pattern'], 'info', 'log2'); - expect(await readLines(PATTERN_FILE_PATH)).to.eql([ - 'CUSTOM - PATTERN [plugins.core_logging.info_pattern][INFO ] log2', - ]); - }); - - it('writes all context to both appenders', async () => { - await setContextConfig(true); - await writeLog(['all'], 'debug', 'log1'); - await writeLog(['all'], 'info', 'log2'); - expect(await readJsonLines()).to.eql([ - { - level: 'DEBUG', - context: 'plugins.core_logging.all', - message: 'log1', - }, - { - level: 'INFO', - context: 'plugins.core_logging.all', - message: 'log2', - }, - ]); - expect(await readLines(PATTERN_FILE_PATH)).to.eql([ - 'CUSTOM - PATTERN [plugins.core_logging.all][DEBUG] log1', - 'CUSTOM - PATTERN [plugins.core_logging.all][INFO ] log2', - ]); - }); - }); -} From 2212beba6847051bd42f759ec2530d7c9521e64d Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 1 Jul 2020 18:48:41 +0300 Subject: [PATCH 17/29] [i18n] integrate new translations + new i18n check #70193 (#70423) Co-authored-by: Elastic Machine --- src/dev/i18n/integrate_locale_files.test.ts | 3 +- src/dev/i18n/integrate_locale_files.ts | 21 +- src/dev/i18n/tasks/check_compatibility.ts | 4 +- src/dev/i18n/utils.js | 22 + src/dev/run_i18n_check.ts | 5 +- src/dev/run_i18n_integrate.ts | 5 +- .../translations/translations/ja-JP.json | 1963 ++-------------- .../translations/translations/zh-CN.json | 1970 ++--------------- 8 files changed, 516 insertions(+), 3477 deletions(-) diff --git a/src/dev/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 7ff1d87f1bc55..3bd3dc61c044f 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -21,7 +21,7 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t import path from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -// @ts-ignore +// @ts-expect-error import { normalizePath } from './utils'; const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); @@ -36,6 +36,7 @@ const defaultIntegrateOptions = { sourceFileName: localePath, dryRun: false, ignoreIncompatible: false, + ignoreMalformed: false, ignoreMissing: false, ignoreUnused: false, config: { diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index d8ccccca15559..f9cd6dd1971c7 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -31,7 +31,8 @@ import { normalizePath, readFileAsync, writeFileAsync, - // @ts-ignore + verifyICUMessage, + // @ts-expect-error } from './utils'; import { I18nConfig } from './config'; @@ -41,6 +42,7 @@ export interface IntegrateOptions { sourceFileName: string; targetFileName?: string; dryRun: boolean; + ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; @@ -105,6 +107,23 @@ export function verifyMessages( } } + for (const messageId of localizedMessagesIds) { + const defaultMessage = defaultMessagesMap.get(messageId); + if (defaultMessage) { + try { + const message = localizedMessagesMap.get(messageId)!; + verifyICUMessage(message); + } catch (err) { + if (options.ignoreMalformed) { + localizedMessagesMap.delete(messageId); + options.log.warning(`Malformed translation ignored (${messageId}): ${err}`); + } else { + errorMessage += `\nMalformed translation (${messageId}): ${err}\n`; + } + } + } + } + if (errorMessage) { throw createFailError(errorMessage); } diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts index 5900bf5aff252..afaf3cd875a8a 100644 --- a/src/dev/i18n/tasks/check_compatibility.ts +++ b/src/dev/i18n/tasks/check_compatibility.ts @@ -22,13 +22,14 @@ import { integrateLocaleFiles, I18nConfig } from '..'; export interface I18nFlags { fix: boolean; + ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; } export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: ToolingLog) { - const { fix, ignoreIncompatible, ignoreUnused, ignoreMissing } = flags; + const { fix, ignoreIncompatible, ignoreUnused, ignoreMalformed, ignoreMissing } = flags; return config.translations.map((translationsPath) => ({ task: async ({ messages }: { messages: Map }) => { // If `fix` is set we should try apply all possible fixes and override translations file. @@ -37,6 +38,7 @@ export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: To ignoreIncompatible: fix || ignoreIncompatible, ignoreUnused: fix || ignoreUnused, ignoreMissing: fix || ignoreMissing, + ignoreMalformed: fix || ignoreMalformed, sourceFileName: translationsPath, targetFileName: fix ? translationsPath : undefined, config, diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 1d1c3118e0852..11a002fdbf4a8 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -208,6 +208,28 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI } } +/** + * Verifies valid ICU message. + * @param message ICU message. + * @param messageId ICU message id + * @returns {undefined} + */ +export function verifyICUMessage(message) { + try { + parser.parse(message); + } catch (error) { + if (error.name === 'SyntaxError') { + const errorWithContext = createParserErrorMessage(message, { + loc: { + line: error.location.start.line, + column: error.location.start.column - 1, + }, + message: error.message, + }); + throw errorWithContext; + } + } +} /** * Extracts value references from the ICU message. * @param message ICU message. diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 97ea988b1de3a..70eeedac2b8b6 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -36,6 +36,7 @@ run( async ({ flags: { 'ignore-incompatible': ignoreIncompatible, + 'ignore-malformed': ignoreMalformed, 'ignore-missing': ignoreMissing, 'ignore-unused': ignoreUnused, 'include-config': includeConfig, @@ -48,12 +49,13 @@ run( fix && (ignoreIncompatible !== undefined || ignoreUnused !== undefined || + ignoreMalformed !== undefined || ignoreMissing !== undefined) ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} none of the --ignore-incompatible, --ignore-unused or --ignore-missing is allowed when --fix is set.` + )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.` ); } @@ -99,6 +101,7 @@ run( checkCompatibility( config, { + ignoreMalformed: !!ignoreMalformed, ignoreIncompatible: !!ignoreIncompatible, ignoreUnused: !!ignoreUnused, ignoreMissing: !!ignoreMissing, diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index 23d66fae9f26e..25c3ea32783aa 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -31,6 +31,7 @@ run( 'ignore-incompatible': ignoreIncompatible = false, 'ignore-missing': ignoreMissing = false, 'ignore-unused': ignoreUnused = false, + 'ignore-malformed': ignoreMalformed = false, 'include-config': includeConfig, path, source, @@ -66,12 +67,13 @@ run( typeof ignoreIncompatible !== 'boolean' || typeof ignoreUnused !== 'boolean' || typeof ignoreMissing !== 'boolean' || + typeof ignoreMalformed !== 'boolean' || typeof dryRun !== 'boolean' ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} --ignore-incompatible, --ignore-unused, --ignore-missing, and --dry-run can't have values` + )} --ignore-incompatible, --ignore-unused, --ignore-malformed, --ignore-missing, and --dry-run can't have values` ); } @@ -97,6 +99,7 @@ run( ignoreIncompatible, ignoreUnused, ignoreMissing, + ignoreMalformed, config, log, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0b466f351d7db..b2533f1bc8c19 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -87,7 +87,6 @@ "advancedSettings.categoryNames.notificationsLabel": "通知", "advancedSettings.categoryNames.reportingLabel": "レポート", "advancedSettings.categoryNames.searchLabel": "検索", - "advancedSettings.categoryNames.securitySolutionLabel": "Security Solution", "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可視化", "advancedSettings.categorySearchLabel": "カテゴリー", @@ -124,6 +123,119 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", + "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", + "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", + "apmOss.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました", + "apmOss.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。", + "apmOss.tutorial.apmAgents.statusCheck.title": "エージェントステータス", + "apmOss.tutorial.apmAgents.title": "APM エージェント", + "apmOss.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。", + "apmOss.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中", + "apmOss.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認", + "apmOss.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。", + "apmOss.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました", + "apmOss.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。", + "apmOss.tutorial.apmServer.statusCheck.title": "APM Server ステータス", + "apmOss.tutorial.apmServer.title": "APM Server", + "apmOss.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します", + "apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。", + "apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", + "apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "apmOss.tutorial.djangoClient.configure.title": "エージェントの構成", + "apmOss.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "apmOss.tutorial.djangoClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、(例: 非 ASP.NET Core アプリケーションの場合)、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:", + "apmOss.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例: 「appsettings.json」ファイル) から構成を読み込みます。", + "apmOss.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。", + "apmOss.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加", + "apmOss.tutorial.dotNetClient.download.textPre": "[NuGet]({allNuGetPackagesLink}) から .NET アプリケーションにエージェントパッケージを追加してください。用途の異なる複数の NuGet パッケージがあります。\n\nEntity Framework Core の ASP.NET Core アプリケーションの場合は、[Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) パッケージをダウンロードしてください。このパッケージは、自動的にすべてのエージェントコンポーネントをアプリケーションに追加します。\n\n 依存性を最低限に抑えたい場合、ASP.NET Core の監視のみに [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) パッケージ、または Entity Framework Core の監視のみに [Elastic.Apm.EfCore]({efCorePackageLink}) パッケージを使用することができます。\n\n 手動インストルメンテーションのみにパブリック Agent API を使用する場合は、[Elastic.Apm]({elasticApmPackageLink}) パッケージを使用してください。", + "apmOss.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード", + "apmOss.tutorial.downloadServer.title": "APM Server をダウンロードして展開します", + "apmOss.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", + "apmOss.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", + "apmOss.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。", + "apmOss.tutorial.editConfig.title": "構成を編集する", + "apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。", + "apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します", + "apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", + "apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "apmOss.tutorial.flaskClient.configure.title": "エージェントの構成", + "apmOss.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "apmOss.tutorial.flaskClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:", + "apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。", + "apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行可能な名前が使用されます。", + "apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。", + "apmOss.tutorial.goClient.configure.title": "エージェントの構成", + "apmOss.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。", + "apmOss.tutorial.goClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。", + "apmOss.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション", + "apmOss.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。", + "apmOss.tutorial.javaClient.download.textPre": "[Maven Central]({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。", + "apmOss.tutorial.javaClient.download.title": "APM エージェントのダウンロード", + "apmOss.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加してエージェントをシステムプロパティで構成します。\n\n * 必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)\n * カスタム APM Server URL (デフォルト: {customApmServerUrl})\n * アプリケーションのベースパッケージを設定します", + "apmOss.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動", + "apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.jsClient.enableRealUserMonitoring.title": "APMサーバーのリアルユーザー監視サポートを有効にする", + "apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)", + "apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要)", + "apmOss.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント]({docLink}) をご覧ください。", + "apmOss.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。", + "apmOss.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ", + "apmOss.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加