diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index fa19719239aa089..6953c146050ebcb 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -127,9 +127,10 @@ steps: - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' + parallelism: 2 agents: queue: n2-4 - timeout_in_minutes: 120 + timeout_in_minutes: 90 key: jest-integration - command: .buildkite/scripts/steps/test/api_integration.sh diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 9b9d8ddfcde6970..d832717906bb1b2 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -127,9 +127,10 @@ steps: - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' + parallelism: 2 agents: queue: n2-4 - timeout_in_minutes: 120 + timeout_in_minutes: 90 key: jest-integration - command: .buildkite/scripts/steps/test/api_integration.sh diff --git a/.buildkite/scripts/steps/test/jest_integration.sh b/.buildkite/scripts/steps/test/jest_integration.sh index d07da0584d46de5..13412881cb6fa7b 100755 --- a/.buildkite/scripts/steps/test/jest_integration.sh +++ b/.buildkite/scripts/steps/test/jest_integration.sh @@ -9,5 +9,5 @@ is_test_execution_step .buildkite/scripts/bootstrap.sh echo '--- Jest Integration Tests' -checks-reporter-with-killswitch "Jest Integration Tests" \ - node --max-old-space-size=6144 scripts/jest_integration --ci +checks-reporter-with-killswitch "Jest Integration Tests $((BUILDKITE_PARALLEL_JOB+1))" \ + .buildkite/scripts/steps/test/jest_parallel.sh jest.integration.config.js diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index c9e0e1aff5cf2f7..bc6184c74eb4a71 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -13,7 +13,7 @@ exitCode=0 while read -r config; do if [ "$((i % JOB_COUNT))" -eq "$JOB" ]; then echo "--- $ node scripts/jest --config $config" - node --max-old-space-size=14336 ./node_modules/.bin/jest --config="$config" --runInBand --coverage=false + node --max-old-space-size=14336 ./node_modules/.bin/jest --config="$config" --runInBand --coverage=false --passWithNoTests lastCode=$? if [ $lastCode -ne 0 ]; then @@ -25,6 +25,6 @@ while read -r config; do ((i=i+1)) # uses heredoc to avoid the while loop being in a sub-shell thus unable to overwrite exitCode -done <<< "$(find src x-pack packages -name jest.config.js -not -path "*/__fixtures__/*" | sort)" +done <<< "$(find src x-pack packages -name ${1:-jest.config.js} -not -path "*/__fixtures__/*" | sort)" -exit $exitCode \ No newline at end of file +exit $exitCode diff --git a/dev_docs/tutorials/debugging.mdx b/dev_docs/tutorials/debugging.mdx index c612893e4f1f93e..598c6119910cb84 100644 --- a/dev_docs/tutorials/debugging.mdx +++ b/dev_docs/tutorials/debugging.mdx @@ -21,7 +21,9 @@ Next we will go over how to exactly enable the inspector for different aspects o You will need to run Jest directly from the Node script: -`node --inspect-brk scripts/jest [TestPathPattern]` +`node --inspect-brk node_modules/.bin/jest --runInBand --config [JestConfig] [TestPathPattern]` + +Additional information can be found in the [Jest troubleshooting documentation](https://jestjs.io/docs/troubleshooting). ### Functional Test Runner diff --git a/package.json b/package.json index 743e12628936639..6ca8f8db7dbb576 100644 --- a/package.json +++ b/package.json @@ -232,7 +232,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.28.0", + "elastic-apm-node": "^3.29.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "expiry-js": "0.1.7", diff --git a/packages/kbn-es/jest.integration.config.js b/packages/kbn-es/jest.integration.config.js new file mode 100644 index 000000000000000..58ed5614f26be25 --- /dev/null +++ b/packages/kbn-es/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/packages/kbn-es'], +}; diff --git a/packages/kbn-optimizer/jest.integration.config.js b/packages/kbn-optimizer/jest.integration.config.js new file mode 100644 index 000000000000000..7357f8f6a34b06c --- /dev/null +++ b/packages/kbn-optimizer/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/packages/kbn-optimizer'], +}; diff --git a/packages/kbn-plugin-generator/jest.integration.config.js b/packages/kbn-plugin-generator/jest.integration.config.js new file mode 100644 index 000000000000000..0eac4b764101a57 --- /dev/null +++ b/packages/kbn-plugin-generator/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/packages/kbn-plugin-generator'], +}; diff --git a/packages/kbn-plugin-helpers/jest.integration.config.js b/packages/kbn-plugin-helpers/jest.integration.config.js new file mode 100644 index 000000000000000..069989abc01e390 --- /dev/null +++ b/packages/kbn-plugin-helpers/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/packages/kbn-plugin-helpers'], +}; diff --git a/packages/kbn-test/jest.integration.config.js b/packages/kbn-test/jest.integration.config.js new file mode 100644 index 000000000000000..091a7a73de484cf --- /dev/null +++ b/packages/kbn-test/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/packages/kbn-test'], +}; diff --git a/packages/kbn-test/jest_integration/jest-preset.js b/packages/kbn-test/jest_integration/jest-preset.js index be007262477d32b..1d665a4e6a16c2c 100644 --- a/packages/kbn-test/jest_integration/jest-preset.js +++ b/packages/kbn-test/jest_integration/jest-preset.js @@ -20,7 +20,13 @@ module.exports = { ], reporters: [ 'default', - ['@kbn/test/target_node/jest/junit_reporter', { reportName: 'Jest Integration Tests' }], + [ + '@kbn/test/target_node/jest/junit_reporter', + { + rootDirectory: '.', + reportName: 'Jest Integration Tests', + }, + ], [ '@kbn/test/target_node/jest/ci_stats_jest_reporter', { diff --git a/packages/kbn-test/src/jest/configs/__snapshots__/jest_configs.test.ts.snap b/packages/kbn-test/src/jest/configs/__snapshots__/jest_configs.test.ts.snap new file mode 100644 index 000000000000000..8de7ea9a4136753 --- /dev/null +++ b/packages/kbn-test/src/jest/configs/__snapshots__/jest_configs.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jestConfigs #expected throws if test file outside root 1`] = `[Error: Test file (bad.test.js) can not exist outside roots (packages/b/nested, packages). Move it to a root or configure additional root.]`; diff --git a/packages/kbn-test/src/jest/configs/index.ts b/packages/kbn-test/src/jest/configs/index.ts new file mode 100644 index 000000000000000..155c385ec761d55 --- /dev/null +++ b/packages/kbn-test/src/jest/configs/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './jest_configs'; diff --git a/packages/kbn-test/src/jest/configs/jest_configs.test.ts b/packages/kbn-test/src/jest/configs/jest_configs.test.ts new file mode 100644 index 000000000000000..4d68733f58d3267 --- /dev/null +++ b/packages/kbn-test/src/jest/configs/jest_configs.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import mockFs from 'mock-fs'; +import fs from 'fs'; + +import { JestConfigs } from './jest_configs'; + +describe('jestConfigs', () => { + let jestConfigs: JestConfigs; + + beforeEach(async () => { + mockFs({ + '/kbn-test/packages': { + a: { + 'jest.config.js': '', + 'a_first.test.js': '', + 'a_second.test.js': '', + }, + b: { + 'b.test.js': '', + integration_tests: { + 'b_integration.test.js': '', + }, + nested: { + d: { + 'd.test.js': '', + }, + }, + }, + c: { + 'jest.integration.config.js': '', + integration_tests: { + 'c_integration.test.js': '', + }, + }, + }, + }); + jestConfigs = new JestConfigs('/kbn-test', ['packages/b/nested', 'packages']); + }); + + afterEach(mockFs.restore); + + describe('#files', () => { + it('lists unit test files', async () => { + const files = await jestConfigs.files('unit'); + expect(files).toEqual([ + 'packages/a/a_first.test.js', + 'packages/a/a_second.test.js', + 'packages/b/b.test.js', + 'packages/b/nested/d/d.test.js', + ]); + }); + + it('lists integration test files', async () => { + const files = await jestConfigs.files('integration'); + expect(files).toEqual([ + 'packages/b/integration_tests/b_integration.test.js', + 'packages/c/integration_tests/c_integration.test.js', + ]); + }); + }); + + describe('#expected', () => { + it('expects unit config files', async () => { + const files = await jestConfigs.expected('unit'); + expect(files).toEqual([ + 'packages/a/jest.config.js', + 'packages/b/jest.config.js', + 'packages/b/nested/d/jest.config.js', + ]); + }); + + it('expects integration config files', async () => { + const files = await jestConfigs.expected('integration'); + expect(files).toEqual([ + 'packages/b/jest.integration.config.js', + 'packages/c/jest.integration.config.js', + ]); + }); + + it('throws if test file outside root', async () => { + fs.writeFileSync('/kbn-test/bad.test.js', ''); + await expect(() => jestConfigs.expected('unit')).rejects.toMatchSnapshot(); + }); + }); + + describe('#existing', () => { + it('lists existing unit test config files', async () => { + const files = await jestConfigs.existing('unit'); + expect(files).toEqual(['packages/a/jest.config.js']); + }); + + it('lists existing integration test config files', async () => { + const files = await jestConfigs.existing('integration'); + expect(files).toEqual(['packages/c/jest.integration.config.js']); + }); + }); + + describe('#missing', () => { + it('lists existing unit test config files', async () => { + const files = await jestConfigs.missing('unit'); + expect(files).toEqual(['packages/b/jest.config.js', 'packages/b/nested/d/jest.config.js']); + }); + + it('lists existing integration test config files', async () => { + const files = await jestConfigs.missing('integration'); + expect(files).toEqual(['packages/b/jest.integration.config.js']); + }); + }); +}); diff --git a/packages/kbn-test/src/jest/configs/jest_configs.ts b/packages/kbn-test/src/jest/configs/jest_configs.ts new file mode 100644 index 000000000000000..a2a55d4a1b64940 --- /dev/null +++ b/packages/kbn-test/src/jest/configs/jest_configs.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import path from 'path'; +import globby from 'globby'; + +// @ts-ignore +import { testMatch } from '../../../jest-preset'; + +export const CONFIG_NAMES = { + unit: 'jest.config.js', + integration: 'jest.integration.config.js', +}; + +export class JestConfigs { + cwd: string; + roots: string[]; + allFiles: string[] | undefined; + + constructor(cwd: string, roots: string[]) { + this.cwd = cwd; + this.roots = roots; + } + + async files(type: 'unit' | 'integration') { + if (!this.allFiles) { + this.allFiles = await globby(testMatch, { + gitignore: true, + cwd: this.cwd, + }); + } + + return this.allFiles.filter((f) => + type === 'integration' ? f.includes('integration_tests') : !f.includes('integration_tests') + ); + } + + async expected(type: 'unit' | 'integration') { + const filesForType = await this.files(type); + const directories: Set = new Set(); + + filesForType.forEach((file) => { + const root = this.roots.find((r) => file.startsWith(r)); + + if (root) { + const splitPath = file.substring(root.length).split(path.sep); + + if (splitPath.length > 2) { + const name = splitPath[1]; + directories.add([root, name].join(path.sep)); + } + } else { + throw new Error( + `Test file (${file}) can not exist outside roots (${this.roots.join( + ', ' + )}). Move it to a root or configure additional root.` + ); + } + }); + + return [...directories].map((d) => [d, CONFIG_NAMES[type]].join(path.sep)); + } + + async existing(type: 'unit' | 'integration') { + return await globby(`**/${CONFIG_NAMES[type]}`, { + gitignore: true, + cwd: this.cwd, + }); + } + + async missing(type: 'unit' | 'integration') { + const expectedConfigs = await this.expected(type); + const existingConfigs = await this.existing(type); + return await expectedConfigs.filter((x) => !existingConfigs.includes(x)); + } + + async allMissing() { + return (await this.missing('unit')).concat(await this.missing('integration')); + } +} diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts index cf37ee82d61e9c5..6f7836e98d34649 100644 --- a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -6,26 +6,29 @@ * Side Public License, v 1. */ -import { relative, resolve, sep } from 'path'; import { writeFileSync } from 'fs'; - -import execa from 'execa'; -import globby from 'globby'; +import path from 'path'; import Mustache from 'mustache'; import { run } from '@kbn/dev-utils'; import { REPO_ROOT } from '@kbn/utils'; -// @ts-ignore -import { testMatch } from '../../jest-preset'; +import { JestConfigs, CONFIG_NAMES } from './configs'; -const template: string = `module.exports = { +const unitTestingTemplate: string = `module.exports = { preset: '@kbn/test', rootDir: '{{{relToRoot}}}', roots: ['/{{{modulePath}}}'], }; `; +const integrationTestingTemplate: string = `module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '{{{relToRoot}}}', + roots: ['/{{{modulePath}}}'], +}; +`; + const roots: string[] = [ 'x-pack/plugins/security_solution/public', 'x-pack/plugins/security_solution/server', @@ -40,68 +43,43 @@ const roots: string[] = [ export async function runCheckJestConfigsCli() { run( async ({ flags: { fix = false }, log }) => { - const { stdout: coveredFiles } = await execa( - 'yarn', - ['--silent', 'jest', '--listTests', '--json'], - { - cwd: REPO_ROOT, - } - ); + const jestConfigs = new JestConfigs(REPO_ROOT, roots); - const allFiles = new Set( - await globby(testMatch.concat(['!**/integration_tests/**']), { - gitignore: true, - }) - ); + const missing = await jestConfigs.allMissing(); - JSON.parse(coveredFiles).forEach((file: string) => { - const pathFromRoot = relative(REPO_ROOT, file); - allFiles.delete(pathFromRoot); - }); - - if (allFiles.size) { + if (missing.length) { log.error( - `The following files do not belong to a jest.config.js file, or that config is not included from the root jest.config.js\n${[ - ...allFiles, + `The following Jest config files do not exist for which there are test files for:\n${[ + ...missing, ] .map((file) => ` - ${file}`) .join('\n')}` ); - } else { - log.success('All test files are included by a Jest configuration'); - return; - } - - if (fix) { - allFiles.forEach((file) => { - const root = roots.find((r) => file.startsWith(r)); - if (root) { - const name = relative(root, file).split(sep)[0]; - const modulePath = [root, name].join('/'); + if (fix) { + missing.forEach((file) => { + const template = file.endsWith(CONFIG_NAMES.unit) + ? unitTestingTemplate + : integrationTestingTemplate; + const modulePath = path.dirname(file); const content = Mustache.render(template, { - relToRoot: relative(modulePath, '.'), + relToRoot: path.relative(modulePath, '.'), modulePath, }); - const configPath = resolve(root, name, 'jest.config.js'); - log.info('created %s', configPath); - writeFileSync(configPath, content); - } else { - log.warning(`Unable to determind where to place jest.config.js for ${file}`); - } - }); - } else { - log.info( - `Run 'node scripts/check_jest_configs --fix' to attempt to create the missing config files` - ); + writeFileSync(file, content); + log.info('created %s', file); + }); + } else { + log.info( + `Run 'node scripts/check_jest_configs --fix' to create the missing config files` + ); + } } - - process.exit(1); }, { - description: 'Check that all test files are covered by a jest.config.js', + description: 'Check that all test files are covered by a Jest config', flags: { boolean: ['fix'], help: ` diff --git a/src/cli/jest.integration.config.js b/src/cli/jest.integration.config.js new file mode 100644 index 000000000000000..96f02d052468841 --- /dev/null +++ b/src/cli/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/src/cli'], +}; diff --git a/src/core/jest.integration.config.js b/src/core/jest.integration.config.js new file mode 100644 index 000000000000000..3b84ae88ad7a73d --- /dev/null +++ b/src/core/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/src/core'], +}; diff --git a/src/dev/jest.integration.config.js b/src/dev/jest.integration.config.js new file mode 100644 index 000000000000000..1225651687834a6 --- /dev/null +++ b/src/dev/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../..', + roots: ['/src/dev'], +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/kibana.json b/src/plugins/chart_expressions/expression_partition_vis/kibana.json index 226d1681cd3fc26..08a030d466eab08 100755 --- a/src/plugins/chart_expressions/expression_partition_vis/kibana.json +++ b/src/plugins/chart_expressions/expression_partition_vis/kibana.json @@ -12,7 +12,7 @@ "extraPublicDirs": [ "common" ], - "requiredPlugins": ["charts", "data", "expressions", "visualizations", "fieldFormats"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "fieldFormats", "presentationUtil"], "requiredBundles": ["kibanaReact"], "optionalPlugins": [] } diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap index b367db1af5437ee..2df18b58134734d 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap @@ -5,33 +5,34 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` css={ Object { "map": undefined, - "name": "1bdmk0u", + "name": "13h2mjc", "next": undefined, "styles": " - display:flex;flex:1 1 auto;min-height:0;min-width:0;;; + + min-height: 0; + min-width: 0; + margin-left: auto; + margin-right: auto; + width: 100%; + height: 100%; +; + inset: 0; position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; padding: 8px; - margin-left: auto; - margin-right: auto; - overflow: hidden; ", "toString": [Function], } } - data-test-subj="visTypePieChart" + data-test-subj="partitionVisChart" >
css` - ${partitionVisWrapperStyle}; +export const partitionVisContainerStyle = css` + min-height: 0; + min-width: 0; + margin-left: auto; + margin-right: auto; + width: 100%; + height: 100%; +`; + +export const partitionVisContainerWithToggleStyleFactory = (theme: EuiThemeComputed) => css` + ${partitionVisContainerStyle} + inset: 0; position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; padding: ${theme.size.s}; - margin-left: auto; - margin-right: auto; - overflow: hidden; `; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx index ddade06c2c7e0f1..001f2390799e670 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx @@ -221,7 +221,9 @@ describe('PartitionVisComponent', function () { } as unknown as Datatable; const newProps = { ...wrapperProps, visData: newVisData }; const component = mount(); - expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual('No results found'); + expect(findTestSubject(component, 'partitionVisEmptyValues').text()).toEqual( + 'No results found' + ); }); it('renders the no results component if there are negative values', () => { @@ -250,8 +252,8 @@ describe('PartitionVisComponent', function () { } as unknown as Datatable; const newProps = { ...wrapperProps, visData: newVisData }; const component = mount(); - expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual( - "Pie/donut charts can't render with negative values." + expect(findTestSubject(component, 'partitionVisNegativeValues').text()).toEqual( + "Pie chart can't render with negative values." ); }); }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index cc96baac3a8ae6e..42a298d00d48c78 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -20,12 +20,7 @@ import { SeriesIdentifier, } from '@elastic/charts'; import { useEuiTheme } from '@elastic/eui'; -import { - LegendToggle, - ClickTriggerEvent, - ChartsPluginSetup, - PaletteRegistry, -} from '../../../../charts/public'; +import { LegendToggle, ChartsPluginSetup, PaletteRegistry } from '../../../../charts/public'; import type { PersistedState } from '../../../../visualizations/public'; import { Datatable, @@ -63,10 +58,12 @@ import { VisualizationNoResults } from './visualization_noresults'; import { VisTypePiePluginStartDependencies } from '../plugin'; import { partitionVisWrapperStyle, - partitionVisContainerStyleFactory, + partitionVisContainerStyle, + partitionVisContainerWithToggleStyleFactory, } from './partition_vis_component.styles'; import { ChartTypes } from '../../common/types'; import { filterOutConfig } from '../utils/filter_out_config'; +import { FilterEvent } from '../types'; declare global { interface Window { @@ -93,7 +90,6 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { const { visData, visParams: preVisParams, visType, services, syncColors } = props; const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]); - const theme = useEuiTheme(); const chartTheme = props.chartsThemeService.useChartsTheme(); const chartBaseTheme = props.chartsThemeService.useChartsBaseTheme(); @@ -103,8 +99,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { ); const formatters = useMemo( - () => generateFormatters(visParams, visData, services.fieldFormats.deserialize), - [services.fieldFormats.deserialize, visData, visParams] + () => generateFormatters(visData, services.fieldFormats.deserialize), + [services.fieldFormats.deserialize, visData] ); const showLegendDefault = useCallback(() => { @@ -114,6 +110,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { const [showLegend, setShowLegend] = useState(() => showLegendDefault()); + const showToggleLegendElement = props.uiState !== undefined; + const [dimensions, setDimensions] = useState(); const parentRef = useRef(null); @@ -157,11 +155,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { splitChartDimension, splitChartFormatter ); - const event = { - name: 'filterBucket', - data: { data }, - }; - props.fireEvent(event); + props.fireEvent({ name: 'filter', data: { data } }); }, [props] ); @@ -169,11 +163,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { // handles legend action event data const getLegendActionEventData = useCallback( (vData: Datatable) => - (series: SeriesIdentifier): ClickTriggerEvent | null => { + (series: SeriesIdentifier): FilterEvent => { const data = getFilterEventData(vData, series); return { - name: 'filterBucket', + name: 'filter', data: { negate: false, data, @@ -184,7 +178,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { ); const handleLegendAction = useCallback( - (event: ClickTriggerEvent, negate = false) => { + (event: FilterEvent, negate = false) => { props.fireEvent({ ...event, data: { @@ -318,6 +312,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { [visData.rows, metricColumn] ); + const isEmpty = visData.rows.length === 0; + const isMetricEmpty = visData.rows.every((row) => !row[metricColumn.id]); + /** * Checks whether data have negative values. * If so, the no data container is loaded. @@ -330,14 +327,23 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { }), [visData.rows, metricColumn] ); + const flatLegend = isLegendFlat(visType, splitChartDimension); - const canShowPieChart = !isAllZeros && !hasNegative; + + const canShowPieChart = !isEmpty && !isMetricEmpty && !isAllZeros && !hasNegative; + + const { euiTheme } = useEuiTheme(); + + const chartContainerStyle = showToggleLegendElement + ? partitionVisContainerWithToggleStyleFactory(euiTheme) + : partitionVisContainerStyle; + const partitionType = getPartitionType(visType); return ( -
+
{!canShowPieChart ? ( - + ) : (
{ distinctColors: visParams.distinctColors ?? false, }} > - + {showToggleLegendElement && ( + + )} { /> { - return ( - - {hasNegativeValues - ? i18n.translate('expressionPartitionVis.negativeValuesFound', { - defaultMessage: "Pie/donut charts can't render with negative values.", - }) - : i18n.translate('expressionPartitionVis.noResultsFoundTitle', { - defaultMessage: 'No results found', - })} - - } - /> - ); +interface Props { + hasNegativeValues?: boolean; + chartType: ChartTypes; +} + +export const VisualizationNoResults: FC = ({ hasNegativeValues = false, chartType }) => { + if (hasNegativeValues) { + const message = ( + + ); + + return ( + + ); + } + + return ; }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx index c3521c7346a81d5..53e729466c1d226 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx @@ -10,35 +10,27 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { Datatable, ExpressionRenderDefinition } from '../../../../expressions/public'; -import { VisualizationContainer } from '../../../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../../../expressions/public'; import type { PersistedState } from '../../../../visualizations/public'; +import { VisTypePieDependencies } from '../plugin'; +import { withSuspense } from '../../../../presentation_util/public'; import { KibanaThemeProvider } from '../../../../kibana_react/public'; - import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants'; import { ChartTypes, RenderValue } from '../../common/types'; -import { VisTypePieDependencies } from '../plugin'; - export const strings = { getDisplayName: () => - i18n.translate('expressionPartitionVis.renderer.pieVis.displayName', { - defaultMessage: 'Pie visualization', + i18n.translate('expressionPartitionVis.renderer.partitionVis.pie.displayName', { + defaultMessage: 'Partition visualization', }), getHelpDescription: () => - i18n.translate('expressionPartitionVis.renderer.pieVis.helpDescription', { - defaultMessage: 'Render a pie', + i18n.translate('expressionPartitionVis.renderer.partitionVis.pie.helpDescription', { + defaultMessage: 'Render pie/donut/treemap/mosaic/waffle charts', }), }; -const PartitionVisComponent = lazy(() => import('../components/partition_vis_component')); - -function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean { - const rows: object[] | undefined = visData?.rows; - const isZeroHits = !rows || !rows.length; - - return Boolean(isZeroHits); -} +const LazyPartitionVisComponent = lazy(() => import('../components/partition_vis_component')); +const PartitionVisComponent = withSuspense(LazyPartitionVisComponent); export const getPartitionVisRenderer: ( deps: VisTypePieDependencies @@ -48,8 +40,6 @@ export const getPartitionVisRenderer: ( help: strings.getHelpDescription(), reuseDomNode: true, render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => { - const showNoResult = shouldShowNoResultsMessage(visData); - handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); @@ -60,7 +50,7 @@ export const getPartitionVisRenderer: ( render( - +
- +
, - domNode + domNode, + () => { + handlers.done(); + } ); }, }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/donut.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/donut.tsx new file mode 100644 index 000000000000000..5846fe0e7e8ba6e --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/donut.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const DonutIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/icons/index.ts new file mode 100644 index 000000000000000..e61bd6557d58174 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { PieIcon } from './pie'; +export { DonutIcon } from './donut'; +export { TreemapIcon } from './treemap'; +export { MosaicIcon } from './mosaic'; +export { WaffleIcon } from './waffle'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/mosaic.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/mosaic.tsx new file mode 100644 index 000000000000000..f8582495f2e0ce8 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/mosaic.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { EuiIconProps } from '@elastic/eui'; + +export const MosaicIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/pie.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/pie.tsx new file mode 100644 index 000000000000000..9176ac3fdd5c18b --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/pie.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const PieIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/treemap.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/treemap.tsx new file mode 100644 index 000000000000000..1860132fa9ffda5 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/treemap.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const TreemapIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/waffle.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/waffle.tsx new file mode 100644 index 000000000000000..30f05dd57f3481d --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/waffle.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { EuiIconProps } from '@elastic/eui'; + +export const WaffleIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/types.ts b/src/plugins/chart_expressions/expression_partition_vis/public/types.ts index 64e132d2ddadb48..aa87124ed2b4b77 100755 --- a/src/plugins/chart_expressions/expression_partition_vis/public/types.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/types.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { ValueClickContext } from '../../../embeddable/public'; import { ChartsPluginSetup } from '../../../charts/public'; import { ExpressionsPublicPlugin, ExpressionsServiceStart } from '../../../expressions/public'; @@ -19,3 +20,8 @@ export interface SetupDeps { export interface StartDeps { expression: ExpressionsServiceStart; } + +export interface FilterEvent { + name: 'filter'; + data: ValueClickContext['data']; +} diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts index 47641a7f270c219..5b48d68f6820150 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts @@ -9,13 +9,13 @@ import { LayerValue, SeriesIdentifier } from '@elastic/charts'; import { Datatable, DatatableColumn } from '../../../../expressions/public'; import { DataPublicPluginStart } from '../../../../data/public'; -import { ClickTriggerEvent } from '../../../../charts/public'; import { ValueClickContext } from '../../../../embeddable/public'; import type { FieldFormat } from '../../../../field_formats/common'; import { BucketColumns } from '../../common/types'; +import { FilterEvent } from '../types'; export const canFilter = async ( - event: ClickTriggerEvent | null, + event: FilterEvent | null, actions: DataPublicPluginStart['actions'] ): Promise => { if (!event) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts index 69443dcfea5fb2c..18f89cb5f3e4e50 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts @@ -8,31 +8,19 @@ import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { Datatable } from '../../../../expressions'; -import { createMockPieParams, createMockVisData } from '../mocks'; +import { createMockVisData } from '../mocks'; import { generateFormatters, getAvailableFormatter, getFormatter } from './formatters'; import { BucketColumns } from '../../common/types'; describe('generateFormatters', () => { - const visParams = createMockPieParams(); const visData = createMockVisData(); const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); beforeEach(() => { defaultFormatter.mockClear(); }); - it('returns empty object, if labels should not be should ', () => { - const formatters = generateFormatters( - { ...visParams, labels: { ...visParams.labels, show: false } }, - visData, - defaultFormatter - ); - - expect(formatters).toEqual({}); - expect(defaultFormatter).toHaveBeenCalledTimes(0); - }); - it('returns formatters, if columns have meta parameters', () => { - const formatters = generateFormatters(visParams, visData, defaultFormatter); + const formatters = generateFormatters(visData, defaultFormatter); const formattingResult = fieldFormatsMock.deserialize(); const serializedFormatters = Object.keys(formatters).reduce( @@ -62,7 +50,7 @@ describe('generateFormatters', () => { columns: visData.columns.map(({ meta, ...col }) => ({ ...col, meta: { type: 'string' } })), }; - const formatters = generateFormatters(visParams, newVisData, defaultFormatter); + const formatters = generateFormatters(newVisData, defaultFormatter); expect(formatters).toEqual({ 'col-0-2': undefined, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts index 59574dd248518da..bbb30169928d4d0 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts @@ -8,25 +8,16 @@ import type { FieldFormat, FormatFactory } from '../../../../field_formats/common'; import type { Datatable } from '../../../../expressions/public'; -import { BucketColumns, PartitionVisParams } from '../../common/types'; +import { BucketColumns } from '../../common/types'; -export const generateFormatters = ( - visParams: PartitionVisParams, - visData: Datatable, - formatFactory: FormatFactory -) => { - if (!visParams.labels.show) { - return {}; - } - - return visData.columns.reduce | undefined>>( +export const generateFormatters = (visData: Datatable, formatFactory: FormatFactory) => + visData.columns.reduce | undefined>>( (newFormatters, column) => ({ ...newFormatters, [column.id]: column?.meta?.params ? formatFactory(column.meta.params) : undefined, }), {} ); -}; export const getAvailableFormatter = ( column: Partial, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_icon.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_icon.ts new file mode 100644 index 000000000000000..cac282553af113f --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_icon.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ChartTypes } from '../../common/types'; +import { PieIcon, DonutIcon, TreemapIcon, MosaicIcon, WaffleIcon } from '../icons'; + +export const getIcon = (chart: ChartTypes) => + ({ + [ChartTypes.PIE]: PieIcon, + [ChartTypes.DONUT]: DonutIcon, + [ChartTypes.TREEMAP]: TreemapIcon, + [ChartTypes.MOSAIC]: MosaicIcon, + [ChartTypes.WAFFLE]: WaffleIcon, + }[chart]); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx index 28b85f6300977c2..72793d771a0ee0a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx @@ -13,16 +13,16 @@ import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } fr import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts'; import { DataPublicPluginStart } from '../../../../data/public'; import { PartitionVisParams } from '../../common/types'; -import { ClickTriggerEvent } from '../../../../charts/public'; import { FieldFormatsStart } from '../../../../field_formats/public'; +import { FilterEvent } from '../types'; export const getLegendActions = ( canFilter: ( - data: ClickTriggerEvent | null, + data: FilterEvent | null, actions: DataPublicPluginStart['actions'] ) => Promise, - getFilterEventData: (series: SeriesIdentifier) => ClickTriggerEvent | null, - onFilter: (data: ClickTriggerEvent, negate?: any) => void, + getFilterEventData: (series: SeriesIdentifier) => FilterEvent | null, + onFilter: (data: FilterEvent, negate?: any) => void, visParams: PartitionVisParams, actions: DataPublicPluginStart['actions'], formatter: FieldFormatsStart diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts index afa0b82a87eb1a3..b0ce92f1205e81a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts @@ -18,3 +18,4 @@ export { getColumnByAccessor } from './accessor'; export { isLegendFlat, shouldShowLegend } from './legend'; export { generateFormatters, getAvailableFormatter, getFormatter } from './formatters'; export { getPartitionType } from './get_partition_type'; +export { getIcon } from './get_icon'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.test.ts new file mode 100644 index 000000000000000..efeb1f038232dc7 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PaletteDefinition, PaletteOutput } from '../../../../../charts/public'; +import { chartPluginMock } from '../../../../../charts/public/mocks'; +import { Datatable } from '../../../../../expressions'; +import { byDataColorPaletteMap } from './get_color'; + +describe('#byDataColorPaletteMap', () => { + let datatable: Datatable; + let paletteDefinition: PaletteDefinition; + let palette: PaletteOutput; + const columnId = 'foo'; + + beforeEach(() => { + datatable = { + rows: [ + { + [columnId]: '1', + }, + { + [columnId]: '2', + }, + ], + } as unknown as Datatable; + paletteDefinition = chartPluginMock.createPaletteRegistry().get('default'); + palette = { type: 'palette' } as PaletteOutput; + }); + + it('should create byDataColorPaletteMap', () => { + expect(byDataColorPaletteMap(datatable.rows, columnId, paletteDefinition, palette)) + .toMatchInlineSnapshot(` + Object { + "getColor": [Function], + } + `); + }); + + it('should get color', () => { + const colorPaletteMap = byDataColorPaletteMap( + datatable.rows, + columnId, + paletteDefinition, + palette + ); + + expect(colorPaletteMap.getColor('1')).toBe('black'); + }); + + it('should return undefined in case if values not in datatable', () => { + const colorPaletteMap = byDataColorPaletteMap( + datatable.rows, + columnId, + paletteDefinition, + palette + ); + + expect(colorPaletteMap.getColor('wrong')).toBeUndefined(); + }); + + it('should increase rankAtDepth for each new value', () => { + const colorPaletteMap = byDataColorPaletteMap( + datatable.rows, + columnId, + paletteDefinition, + palette + ); + colorPaletteMap.getColor('1'); + colorPaletteMap.getColor('2'); + + expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( + 1, + [{ name: '1', rankAtDepth: 0, totalSeriesAtDepth: 2 }], + { behindText: false }, + undefined + ); + + expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( + 2, + [{ name: '2', rankAtDepth: 1, totalSeriesAtDepth: 2 }], + { behindText: false }, + undefined + ); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.test.ts new file mode 100644 index 000000000000000..1ccfdb7a5b1f9a0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Datatable } from '../../../../../expressions'; +import { extractUniqTermsMap } from './sort_predicate'; + +describe('#extractUniqTermsMap', () => { + it('should extract map', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'string' } }, + { id: 'c', name: 'C', meta: { type: 'number' } }, + ], + rows: [ + { a: 'Hi', b: 'Two', c: 2 }, + { a: 'Test', b: 'Two', c: 5 }, + { a: 'Foo', b: 'Three', c: 6 }, + ], + }; + expect(extractUniqTermsMap(table, 'a')).toMatchInlineSnapshot(` + Object { + "Foo": 2, + "Hi": 0, + "Test": 1, + } + `); + expect(extractUniqTermsMap(table, 'b')).toMatchInlineSnapshot(` + Object { + "Three": 1, + "Two": 0, + } + `); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json index d480d7d27df5a51..97a0c8a9fc51568 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json +++ b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json @@ -15,6 +15,7 @@ "references": [ { "path": "../../../core/tsconfig.json" }, { "path": "../../expressions/tsconfig.json" }, + { "path": "../../presentation_util/tsconfig.json" }, { "path": "../../data/tsconfig.json" }, { "path": "../../field_formats/tsconfig.json" }, { "path": "../../charts/tsconfig.json" }, diff --git a/src/plugins/chart_expressions/jest.config.js b/src/plugins/chart_expressions/jest.config.js new file mode 100644 index 000000000000000..503ef441c03590e --- /dev/null +++ b/src/plugins/chart_expressions/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/chart_expressions'], +}; diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx index e376120c9cd9e65..6989ea7a7a63b2f 100644 --- a/src/plugins/charts/public/static/components/empty_placeholder.tsx +++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx @@ -13,14 +13,24 @@ import './empty_placeholder.scss'; export const EmptyPlaceholder = ({ icon, + iconColor = 'subdued', message = , + dataTestSubj = 'emptyPlaceholder', }: { icon: IconType; + iconColor?: string; message?: JSX.Element; + dataTestSubj?: string; }) => ( <> - - + +

{message}

diff --git a/src/plugins/expressions/common/execution/execution_contract.test.ts b/src/plugins/expressions/common/execution/execution_contract.test.ts index de209f1dfb4a19b..6b0fa0d0db592f9 100644 --- a/src/plugins/expressions/common/execution/execution_contract.test.ts +++ b/src/plugins/expressions/common/execution/execution_contract.test.ts @@ -6,11 +6,13 @@ * Side Public License, v 1. */ +import { Observable, Subscriber } from 'rxjs'; import { first } from 'rxjs/operators'; import { Execution } from './execution'; import { parseExpression } from '../ast'; import { createUnitTestExecutor } from '../test_helpers'; import { ExecutionContract } from './execution_contract'; +import { ExpressionFunctionDefinition } from '../expression_functions'; const createExecution = ( expression: string = 'foo bar=123', @@ -117,11 +119,40 @@ describe('ExecutionContract', () => { const contract = new ExecutionContract(execution); execution.start(); - await execution.result.pipe(first()).toPromise(); execution.state.get().state = 'error'; expect(contract.isPending).toBe(false); expect(execution.state.get().state).toBe('error'); }); + + test('is true when execution is in progress but got partial result, is false once we get final result', async () => { + let mySubscriber: Subscriber; + const arg = new Observable((subscriber) => { + mySubscriber = subscriber; + subscriber.next(1); + }); + + const observable: ExpressionFunctionDefinition<'observable', unknown, {}, unknown> = { + name: 'observable', + args: {}, + help: '', + fn: () => arg, + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable); + + const execution = executor.createExecution('observable'); + execution.start(null); + await execution.result.pipe(first()).toPromise(); + + expect(execution.contract.isPending).toBe(true); + expect(execution.state.get().state).toBe('result'); + + mySubscriber!.next(2); + mySubscriber!.complete(); + + expect(execution.contract.isPending).toBe(false); + expect(execution.state.get().state).toBe('result'); + }); }); }); diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts index 69587c58f104529..51678685823327b 100644 --- a/src/plugins/expressions/common/execution/execution_contract.ts +++ b/src/plugins/expressions/common/execution/execution_contract.ts @@ -19,8 +19,8 @@ import { Adapters } from '../../../inspector/common/adapters'; */ export class ExecutionContract { public get isPending(): boolean { - const state = this.execution.state.get().state; - const finished = state === 'error' || state === 'result'; + const { state, result } = this.execution.state.get(); + const finished = state === 'error' || (state === 'result' && !result?.partial); return !finished; } diff --git a/src/plugins/kibana_usage_collection/jest.integration.config.js b/src/plugins/kibana_usage_collection/jest.integration.config.js new file mode 100644 index 000000000000000..b4edb79789bbe95 --- /dev/null +++ b/src/plugins/kibana_usage_collection/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/src/plugins/kibana_usage_collection'], +}; diff --git a/src/plugins/usage_collection/jest.integration.config.js b/src/plugins/usage_collection/jest.integration.config.js new file mode 100644 index 000000000000000..b63bcb880a642c5 --- /dev/null +++ b/src/plugins/usage_collection/jest.integration.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/src/plugins/usage_collection'], +}; diff --git a/src/plugins/vis_types/jest.config.js b/src/plugins/vis_types/jest.config.js new file mode 100644 index 000000000000000..af7f2b462b89f3c --- /dev/null +++ b/src/plugins/vis_types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_types'], +}; diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/get_request_params.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/get_request_params.ts index 41f7e7c86708f76..815598007030dcc 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/get_request_params.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/get_request_params.ts @@ -31,6 +31,7 @@ export async function getAnnotationRequestParams( capabilities, uiSettings, cachedIndexPatternFetcher, + buildSeriesMetaParams, }: AnnotationServices ): Promise { const annotationIndex = await cachedIndexPatternFetcher(annotation.index_pattern); @@ -43,6 +44,7 @@ export async function getAnnotationRequestParams( annotationIndex, capabilities, uiSettings, + getMetaParams: () => buildSeriesMetaParams(annotationIndex, Boolean(panel.use_kibana_indexes)), }); return { diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts index a52e15eb90feebd..c1bd0a11f550a93 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts @@ -23,16 +23,26 @@ export const dateHistogram: AnnotationsRequestProcessorsFunction = ({ annotationIndex, capabilities, uiSettings, + getMetaParams, }) => { return (next) => async (doc) => { + const maxBarsUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const timeField = annotation.time_field || annotationIndex.indexPattern?.timeFieldName || ''; + const { interval, maxBars } = await getMetaParams(); if (panel.use_kibana_indexes) { validateField(timeField, annotationIndex); } const { bucketSize, intervalString } = getBucketSize( + req, + interval, + capabilities, + maxBars ? Math.min(maxBarsUiSettings, maxBars) : barTargetUiSettings + ); + + const { bucketSize: autoBucketSize, intervalString: autoIntervalString } = getBucketSize( req, 'auto', capabilities, @@ -49,7 +59,7 @@ export const dateHistogram: AnnotationsRequestProcessorsFunction = ({ min: from.valueOf(), max: to.valueOf() - bucketSize * 1000, }, - ...dateHistogramInterval(intervalString), + ...dateHistogramInterval(autoBucketSize < bucketSize ? autoIntervalString : intervalString), }); return next(doc); }; diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/types.ts index 0b67d6f0d19843d..ae2563fbfb64b86 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/types.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/types.ts @@ -22,6 +22,11 @@ export interface AnnotationsRequestProcessorsParams { annotationIndex: FetchedIndexPattern; capabilities: SearchCapabilities; uiSettings: IUiSettingsClient; + getMetaParams: () => Promise<{ + maxBars: number; + timeField?: string | undefined; + interval: string; + }>; } export type AnnotationSearchRequest = Record; diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index 2e21b2e1f8ec6ca..23325ef5aa084f4 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -31,7 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.save(); }; - describe('discover integration with runtime fields editor', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/123372 + describe.skip('discover integration with runtime fields editor', function describeIndexTests() { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/_pie_chart.ts index 744ba3caa719e4e..48d49d3007b685b 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/_pie_chart.ts @@ -432,7 +432,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { '360,000', 'CN', ].sort(); - if (await PageObjects.visChart.isNewLibraryChart('visTypePieChart')) { + if (await PageObjects.visChart.isNewLibraryChart('partitionVisChart')) { await PageObjects.visEditor.clickOptionsTab(); await PageObjects.visEditor.togglePieLegend(); await PageObjects.visEditor.togglePieNestedLegend(); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 4fa8cd6f2d7f596..d21581fba56d71c 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -468,12 +468,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.setAnnotationFilter('geo.dest : "AW" or geo.src : "AM"'); await visualBuilder.setAnnotationFields('extension.raw'); await visualBuilder.setAnnotationRowTemplate('extension: {{extension.raw}}'); - const annotationsData = await visualBuilder.getAnnotationsData(); - expect(annotationsData).to.eql(expectedAnnotationsData); }); - it('should display correct annotations data for machine.os.raw and memory fields', async () => { const expectedAnnotationsData = [ { @@ -512,12 +509,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.setAnnotationRowTemplate( 'OS: {{machine.os.raw}}, memory: {{memory}}' ); - const annotationsData = await visualBuilder.getAnnotationsData(); - expect(annotationsData).to.eql(expectedAnnotationsData); }); - it('should display correct annotations data when using runtime field', async () => { const expectedAnnotationsData = [ { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 60d7c6e7d7435e9..3eec4e2ce1a2bfc 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -11,7 +11,7 @@ import chroma from 'chroma-js'; import { FtrService } from '../ftr_provider_context'; -const pieChartSelector = 'visTypePieChart'; +const partitionVisChartSelector = 'partitionVisChart'; const heatmapChartSelector = 'heatmapChart'; export class VisualizeChartPageObject extends FtrService { @@ -149,7 +149,7 @@ export class VisualizeChartPageObject extends FtrService { } private async toggleLegend(force = false) { - const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector); + const isVisTypePieChart = await this.isNewLibraryChart(partitionVisChartSelector); const legendSelector = force || isVisTypePieChart ? '.echLegend' : '.visLegend'; await this.retry.try(async () => { @@ -182,10 +182,11 @@ export class VisualizeChartPageObject extends FtrService { } public async doesSelectedLegendColorExistForPie(matchingColor: string) { - if (await this.isNewLibraryChart(pieChartSelector)) { + if (await this.isNewLibraryChart(partitionVisChartSelector)) { const hexMatchingColor = chroma(matchingColor).hex().toUpperCase(); const slices = - (await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? []; + (await this.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]?.partitions ?? + []; return slices.some(({ color }) => { return hexMatchingColor === chroma(color).hex().toUpperCase(); }); @@ -195,7 +196,7 @@ export class VisualizeChartPageObject extends FtrService { } public async expectError() { - if (!this.isNewLibraryChart(pieChartSelector)) { + if (!this.isNewLibraryChart(partitionVisChartSelector)) { await this.testSubjects.existOrFail('vislibVisualizeError'); } } @@ -244,12 +245,13 @@ export class VisualizeChartPageObject extends FtrService { } public async getLegendEntries() { - const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector); + const isVisTypePieChart = await this.isNewLibraryChart(partitionVisChartSelector); const isVisTypeHeatmapChart = await this.isNewLibraryChart(heatmapChartSelector); if (isVisTypePieChart) { const slices = - (await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? []; + (await this.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]?.partitions ?? + []; return slices.map(({ name }) => name); } @@ -290,7 +292,7 @@ export class VisualizeChartPageObject extends FtrService { public async openLegendOptionColorsForPie(name: string, chartSelector: string) { await this.waitForVisualizationRenderingStabilized(); await this.retry.try(async () => { - if (await this.isNewLibraryChart(pieChartSelector)) { + if (await this.isNewLibraryChart(partitionVisChartSelector)) { const chart = await this.find.byCssSelector(chartSelector); const legendItemColor = await chart.findByCssSelector( `[data-ech-series-name="${name}"] .echLegendItem__color` diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index ff0c24e2830cfae..16133140e4abf05 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { isNil } from 'lodash'; import { FtrService } from '../../ftr_provider_context'; -const pieChartSelector = 'visTypePieChart'; +const partitionVisChartSelector = 'partitionVisChart'; export class PieChartService extends FtrService { private readonly log = this.ctx.getService('log'); @@ -27,16 +27,16 @@ export class PieChartService extends FtrService { async clickOnPieSlice(name?: string) { this.log.debug(`PieChart.clickOnPieSlice(${name})`); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; let sliceLabel = name || slices[0].name; if (name === 'Other') { sliceLabel = '__other__'; } const pieSlice = slices.find((slice) => slice.name === sliceLabel); - const pie = await this.testSubjects.find(pieChartSelector); + const pie = await this.testSubjects.find(partitionVisChartSelector); if (pieSlice) { const pieSize = await pie.getSize(); const pieHeight = pieSize.height; @@ -88,10 +88,10 @@ export class PieChartService extends FtrService { async getPieSliceStyle(name: string) { this.log.debug(`VisualizePage.getPieSliceStyle(${name})`); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; const selectedSlice = slices.filter((slice) => { return slice.name.toString() === name.replace(',', ''); }); @@ -103,10 +103,10 @@ export class PieChartService extends FtrService { async getAllPieSliceColor(name: string) { this.log.debug(`VisualizePage.getAllPieSliceColor(${name})`); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; const selectedSlice = slices.filter((slice) => { return slice.name.toString() === name.replace(',', ''); }); @@ -143,10 +143,10 @@ export class PieChartService extends FtrService { } async getPieChartLabels() { - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; return slices.map((slice) => { if (slice.name === '__missing__') { return 'Missing'; @@ -169,10 +169,10 @@ export class PieChartService extends FtrService { async getPieSliceCount() { this.log.debug('PieChart.getPieSliceCount'); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; return slices?.length; } const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice'); @@ -181,8 +181,8 @@ export class PieChartService extends FtrService { async expectPieSliceCountEsCharts(expectedCount: number) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; expect(slices.length).to.be(expectedCount); } diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/index.ts b/x-pack/plugins/fleet/jest.integration.config.js similarity index 59% rename from x-pack/plugins/lens/common/expressions/pie_chart/index.ts rename to x-pack/plugins/fleet/jest.integration.config.js index 1c1f6fdae457833..f1b9ee2f5f7e0bb 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/index.ts +++ b/x-pack/plugins/fleet/jest.integration.config.js @@ -5,12 +5,8 @@ * 2.0. */ -export { pie } from './pie_chart'; - -export type { - SharedPieLayerState, - PieLayerState, - PieVisualizationState, - PieExpressionArgs, - PieExpressionProps, -} from './types'; +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/x-pack/plugins/fleet'], +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts index 3d928bed0f6618b..097cbd551fad5a6 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts @@ -43,6 +43,15 @@ describe('getMonitoringPermissions', () => { ); expect(permissions).toMatchSnapshot(); }); + + it('should an empty valid permission entry if neither metrics and logs are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: false, metrics: false }, + 'testnamespace123' + ); + expect(permissions).toEqual({ _elastic_agent_monitoring: { indices: [] } }); + }); }); describe('With elastic agent package installed', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts index 3533d829e134259..7e897d62c8be924 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts @@ -30,6 +30,14 @@ function buildDefault(enabled: { logs: boolean; metrics: boolean }, namespace: s ); } + if (names.length === 0) { + return { + _elastic_agent_monitoring: { + indices: [], + }, + }; + } + return { _elastic_agent_monitoring: { indices: [ diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 8cfb2844159bca0..5996ce5404b709b 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -116,10 +116,8 @@ function setKibanaVersion(url: URL) { } const kibanaVersion = appContextService.getKibanaVersion().split('-')[0]; // may be x.y.z-SNAPSHOT - const kibanaBranch = appContextService.getKibanaBranch(); - // on main, request all packages regardless of version - if (kibanaVersion && kibanaBranch !== 'main') { + if (kibanaVersion) { url.searchParams.set('kibana.version', kibanaVersion); } } diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 3e185de8f86187a..cb93933bb0d0577 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -103,6 +103,8 @@ class PackagePolicyService { overwrite?: boolean; } ): Promise { + // trailing whitespace causes issues creating API keys + packagePolicy.name = packagePolicy.name.trim(); if (!options?.skipUniqueNameVerification) { const existingPoliciesWithName = await this.list(soClient, { perPage: 1, @@ -365,6 +367,7 @@ class PackagePolicyService { options?: { user?: AuthenticatedUser }, currentVersion?: string ): Promise { + packagePolicy.name = packagePolicy.name.trim(); const oldPackagePolicy = await this.get(soClient, id); const { version, ...restOfPackagePolicy } = packagePolicy; diff --git a/x-pack/plugins/global_search/jest.integration.config.js b/x-pack/plugins/global_search/jest.integration.config.js new file mode 100644 index 000000000000000..6fb4e4bfe6d68b8 --- /dev/null +++ b/x-pack/plugins/global_search/jest.integration.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/x-pack/plugins/global_search'], +}; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index f0db3385cefc1b2..bd507be52e2ab9c 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -17,6 +17,32 @@ export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const BASE_API_URL = '/api/lens'; export const LENS_EDIT_BY_VALUE = 'edit_by_value'; +export const PieChartTypes = { + PIE: 'pie', + DONUT: 'donut', + TREEMAP: 'treemap', + MOSAIC: 'mosaic', + WAFFLE: 'waffle', +} as const; + +export const CategoryDisplay = { + DEFAULT: 'default', + INSIDE: 'inside', + HIDE: 'hide', +} as const; + +export const NumberDisplay = { + HIDDEN: 'hidden', + PERCENT: 'percent', + VALUE: 'value', +} as const; + +export const LegendDisplay = { + DEFAULT: 'default', + SHOW: 'show', + HIDE: 'hide', +} as const; + export const layerTypes: Record = { DATA: 'data', REFERENCELINE: 'referenceLine', diff --git a/x-pack/plugins/lens/common/expressions/index.ts b/x-pack/plugins/lens/common/expressions/index.ts index c5ee16ed4bcfda3..d7c27c4436b4226 100644 --- a/x-pack/plugins/lens/common/expressions/index.ts +++ b/x-pack/plugins/lens/common/expressions/index.ts @@ -12,7 +12,6 @@ export * from './merge_tables'; export * from './time_scale'; export * from './datatable'; export * from './metric_chart'; -export * from './pie_chart'; export * from './xy_chart'; export * from './expression_types'; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts deleted file mode 100644 index feec2117632c0ed..000000000000000 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Position } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; - -import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; -import type { LensMultiTable } from '../../types'; -import type { PieExpressionProps, PieExpressionArgs } from './types'; - -interface PieRender { - type: 'render'; - as: 'lens_pie_renderer'; - value: PieExpressionProps; -} - -export const pie: ExpressionFunctionDefinition< - 'lens_pie', - LensMultiTable, - PieExpressionArgs, - PieRender -> = { - name: 'lens_pie', - type: 'render', - help: i18n.translate('xpack.lens.pie.expressionHelpLabel', { - defaultMessage: 'Pie renderer', - }), - args: { - title: { - types: ['string'], - help: 'The chart title.', - }, - description: { - types: ['string'], - help: '', - }, - groups: { - types: ['string'], - multi: true, - help: '', - }, - metric: { - types: ['string'], - help: '', - }, - shape: { - types: ['string'], - options: ['pie', 'donut', 'treemap', 'mosaic'], - help: '', - }, - hideLabels: { - types: ['boolean'], - help: '', - }, - numberDisplay: { - types: ['string'], - options: ['hidden', 'percent', 'value'], - help: '', - }, - categoryDisplay: { - types: ['string'], - options: ['default', 'inside', 'hide'], - help: '', - }, - legendDisplay: { - types: ['string'], - options: ['default', 'show', 'hide'], - help: '', - }, - nestedLegend: { - types: ['boolean'], - help: '', - }, - legendMaxLines: { - types: ['number'], - help: '', - }, - truncateLegend: { - types: ['boolean'], - help: '', - }, - showValuesInLegend: { - types: ['boolean'], - help: '', - }, - legendPosition: { - types: ['string'], - options: [Position.Top, Position.Right, Position.Bottom, Position.Left], - help: '', - }, - percentDecimals: { - types: ['number'], - help: '', - }, - palette: { - default: `{theme "palette" default={system_palette name="default"} }`, - help: '', - types: ['palette'], - }, - emptySizeRatio: { - types: ['number'], - help: '', - }, - ariaLabel: { - types: ['string'], - help: '', - required: false, - }, - }, - inputTypes: ['lens_multitable'], - fn(data: LensMultiTable, args: PieExpressionArgs, handlers) { - return { - type: 'render', - as: 'lens_pie_renderer', - value: { - data, - args: { - ...args, - ariaLabel: - args.ariaLabel ?? - (handlers.variables?.embeddableTitle as string) ?? - handlers.getExecutionContext?.()?.description, - }, - }, - }; - }, -}; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts deleted file mode 100644 index aa84488dbc2c22d..000000000000000 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { PaletteOutput } from '../../../../../../src/plugins/charts/common'; -import type { LensMultiTable, LayerType } from '../../types'; - -export type PieChartTypes = 'donut' | 'pie' | 'treemap' | 'mosaic' | 'waffle'; - -export interface SharedPieLayerState { - groups: string[]; - metric?: string; - numberDisplay: 'hidden' | 'percent' | 'value'; - categoryDisplay: 'default' | 'inside' | 'hide'; - legendDisplay: 'default' | 'show' | 'hide'; - legendPosition?: 'left' | 'right' | 'top' | 'bottom'; - showValuesInLegend?: boolean; - nestedLegend?: boolean; - percentDecimals?: number; - emptySizeRatio?: number; - legendMaxLines?: number; - truncateLegend?: boolean; -} - -export type PieLayerState = SharedPieLayerState & { - layerId: string; - layerType: LayerType; -}; - -export interface PieVisualizationState { - shape: PieChartTypes; - layers: PieLayerState[]; - palette?: PaletteOutput; -} - -export type PieExpressionArgs = SharedPieLayerState & { - title?: string; - description?: string; - shape: PieChartTypes; - hideLabels: boolean; - palette: PaletteOutput; - ariaLabel?: string; -}; - -export interface PieExpressionProps { - data: LensMultiTable; - args: PieExpressionArgs; -} diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index f3572fea90f9e09..0b2b5d5d739d02c 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -6,12 +6,16 @@ */ import type { Filter, FilterMeta } from '@kbn/es-query'; +import { Position } from '@elastic/charts'; +import { $Values } from '@kbn/utility-types'; import type { IFieldFormat, SerializedFieldFormat, } from '../../../../src/plugins/field_formats/common'; import type { Datatable } from '../../../../src/plugins/expressions/common'; import type { PaletteContinuity } from '../../../../src/plugins/charts/common'; +import type { PaletteOutput } from '../../../../src/plugins/charts/common'; +import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; @@ -73,3 +77,41 @@ export type LayerType = 'data' | 'referenceLine'; // Shared by XY Chart and Heatmap as for now export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; + +export type PieChartType = $Values; +export type CategoryDisplayType = $Values; +export type NumberDisplayType = $Values; + +export type LegendDisplayType = $Values; + +export enum EmptySizeRatios { + SMALL = 0.3, + MEDIUM = 0.54, + LARGE = 0.7, +} + +export interface SharedPieLayerState { + groups: string[]; + metric?: string; + numberDisplay: NumberDisplayType; + categoryDisplay: CategoryDisplayType; + legendDisplay: LegendDisplayType; + legendPosition?: Position; + showValuesInLegend?: boolean; + nestedLegend?: boolean; + percentDecimals?: number; + emptySizeRatio?: number; + legendMaxLines?: number; + truncateLegend?: boolean; +} + +export type PieLayerState = SharedPieLayerState & { + layerId: string; + layerType: LayerType; +}; + +export interface PieVisualizationState { + shape: $Values; + layers: PieLayerState[]; + palette?: PaletteOutput; +} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index b8fd06a09ebcd49..482a5b931ed78af 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -23,7 +23,8 @@ import type { LensByReferenceInput, LensByValueInput } from './embeddable'; import type { Document } from '../persistence'; import type { IndexPatternPersistedState } from '../indexpattern_datasource/types'; import type { XYState } from '../xy_visualization/types'; -import type { PieVisualizationState, MetricState } from '../../common/expressions'; +import type { MetricState } from '../../common/expressions'; +import type { PieVisualizationState } from '../../common'; import type { DatatableVisualizationState } from '../datatable_visualization/visualization'; import type { HeatmapVisualizationState } from '../heatmap_visualization/types'; import type { GaugeVisualizationState } from '../visualizations/gauge/constants'; diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index 22e43addefcddad..2e5a30345633f8c 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -24,7 +24,6 @@ import { datatableColumn } from '../common/expressions/datatable/datatable_colum import { mergeTables } from '../common/expressions/merge_tables'; import { renameColumns } from '../common/expressions/rename_columns/rename_columns'; -import { pie } from '../common/expressions/pie_chart/pie_chart'; import { formatColumn } from '../common/expressions/format_column'; import { counterRate } from '../common/expressions/counter_rate'; import { getTimeScale } from '../common/expressions/time_scale/time_scale'; @@ -39,7 +38,6 @@ export const setupExpressions = ( [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); [ - pie, xyChart, mergeTables, counterRate, diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 1c045e63e9e26c7..f6ccb071075acff 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -14,9 +14,6 @@ export type { export type { XYState } from './xy_visualization/types'; export type { DataType, OperationMetadata, Visualization } from './types'; export type { - PieVisualizationState, - PieLayerState, - SharedPieLayerState, MetricState, AxesSettingsConfig, XYLayerConfig, @@ -26,7 +23,13 @@ export type { XYCurveType, YConfig, } from '../common/expressions'; -export type { ValueLabelConfig } from '../common/types'; +export type { + ValueLabelConfig, + PieVisualizationState, + PieLayerState, + SharedPieLayerState, +} from '../common/types'; + export type { DatatableVisualizationState } from './datatable_visualization/visualization'; export type { HeatmapVisualizationState } from './heatmap_visualization/types'; export type { GaugeVisualizationState } from './visualizations/gauge/constants'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 4c656d15f197fa9..d574f9f6c5d352f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -18,6 +18,7 @@ import { htmlIdGenerator, EuiButtonGroup, } from '@elastic/eui'; +import { uniq } from 'lodash'; import { AggFunctionsMapping } from '../../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; import { updateColumnParam, updateDefaultLabels } from '../../layer_helpers'; @@ -367,12 +368,21 @@ export const termsOperation: OperationDefinition { const column = layer.columns[columnId] as TermsIndexPatternColumn; const secondaryFields = fields.length > 1 ? fields.slice(1) : undefined; + const dataTypes = uniq(fields.map((field) => indexPattern.getFieldByName(field)?.type)); + const newDataType = (dataTypes.length === 1 ? dataTypes[0] : 'string') || column.dataType; + const newParams = { + ...column.params, + }; + if ('format' in newParams && newDataType !== 'number') { + delete newParams.format; + } updateLayer({ ...layer, columns: { ...layer.columns, [columnId]: { ...column, + dataType: newDataType, sourceField: fields[0], label: ofName( indexPattern.getFieldByName(fields[0])?.displayName, @@ -380,10 +390,10 @@ export const termsOperation: OperationDefinition => ({ - name: 'lens_pie_renderer', - displayName: i18n.translate('xpack.lens.pie.visualizationName', { - defaultMessage: 'Pie', - }), - help: '', - validate: () => undefined, - reuseDomNode: true, - render: (domNode: Element, config: PieExpressionProps, handlers: IInterpreterRenderHandlers) => { - const onClickValue = (data: LensFilterEvent['data']) => { - handlers.event({ name: 'filter', data }); - }; - - ReactDOM.render( - - - - - , - domNode, - () => { - handlers.done(); - } - ); - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - }, -}); - -const MemoizedChart = React.memo(PieComponent); diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx deleted file mode 100644 index df0648aa40d7419..000000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { LegendActionProps, SeriesIdentifier } from '@elastic/charts'; -import { EuiPopover } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { ComponentType, ReactWrapper } from 'enzyme'; -import type { Datatable } from 'src/plugins/expressions/public'; -import { getLegendAction } from './get_legend_action'; -import { LegendActionPopover } from '../shared_components'; - -const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 2 }, - { a: 'Test', b: 4 }, - { a: 'Foo', b: 6 }, - ], -}; - -describe('getLegendAction', function () { - let wrapperProps: LegendActionProps; - const Component: ComponentType = getLegendAction(table, jest.fn()); - let wrapper: ReactWrapper; - - beforeAll(() => { - wrapperProps = { - color: 'rgb(109, 204, 177)', - label: 'Bar', - series: [ - { - specId: 'donut', - key: 'Bar', - }, - ] as unknown as SeriesIdentifier[], - }; - }); - - it('is not rendered if row does not exist', () => { - wrapper = mountWithIntl(); - expect(wrapper).toEqual({}); - expect(wrapper.find(EuiPopover).length).toBe(0); - }); - - it('is rendered if row is detected', () => { - const newProps = { - ...wrapperProps, - label: 'Hi', - series: [ - { - specId: 'donut', - key: 'Hi', - }, - ] as unknown as SeriesIdentifier[], - }; - wrapper = mountWithIntl(); - expect(wrapper.find(EuiPopover).length).toBe(1); - expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options'); - expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ - data: [ - { - column: 0, - row: 0, - table, - value: 'Hi', - }, - ], - }); - }); -}); diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx deleted file mode 100644 index 9f16ad863a41553..000000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import type { LegendAction } from '@elastic/charts'; -import type { Datatable } from 'src/plugins/expressions/public'; -import type { LensFilterEvent } from '../types'; -import { LegendActionPopover } from '../shared_components'; - -export const getLegendAction = ( - table: Datatable, - onFilter: (data: LensFilterEvent['data']) => void -): LegendAction => - React.memo(({ series: [pieSeries], label }) => { - const data = table.columns.reduce((acc, { id }, column) => { - const value = pieSeries.key; - const row = table.rows.findIndex((r) => r[id] === value); - if (row > -1) { - acc.push({ - table, - column, - row, - value, - }); - } - - return acc; - }, []); - - if (data.length === 0) { - return null; - } - - const context: LensFilterEvent['data'] = { - data, - }; - - return ; - }); diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts index ce54f53c1cc93f4..b86c2fc90e4fa6e 100644 --- a/x-pack/plugins/lens/public/pie_visualization/index.ts +++ b/x-pack/plugins/lens/public/pie_visualization/index.ts @@ -6,16 +6,12 @@ */ import type { CoreSetup } from 'src/core/public'; -import type { ExpressionsSetup } from 'src/plugins/expressions/public'; import type { EditorFrameSetup } from '../types'; import type { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -import type { FormatFactory } from '../../common'; export interface PieVisualizationPluginSetupPlugins { editorFrame: EditorFrameSetup; - expressions: ExpressionsSetup; - formatFactory: FormatFactory; charts: ChartsPluginSetup; } @@ -24,22 +20,11 @@ export interface PieVisualizationPluginStartPlugins { } export class PieVisualization { - setup( - core: CoreSetup, - { expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins - ) { + setup(core: CoreSetup, { editorFrame, charts }: PieVisualizationPluginSetupPlugins) { editorFrame.registerVisualization(async () => { - const { getPieVisualization, getPieRenderer } = await import('../async_services'); + const { getPieVisualization } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - expressions.registerRenderer( - getPieRenderer({ - formatFactory, - chartsThemeService: charts.theme, - paletteService: palettes, - kibanaTheme: core.theme, - }) - ); return getPieVisualization({ paletteService: palettes, kibanaTheme: core.theme }); }); } diff --git a/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts b/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts index 3d02c0f6d513ef0..d77a09ae1068974 100644 --- a/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts +++ b/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts @@ -6,24 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { ArrayEntry, PartitionLayout } from '@elastic/charts'; import type { EuiIconProps } from '@elastic/eui'; +import type { DatatableColumn } from '../../../../../src/plugins/expressions'; import { LensIconChartDonut } from '../assets/chart_donut'; import { LensIconChartPie } from '../assets/chart_pie'; import { LensIconChartTreemap } from '../assets/chart_treemap'; import { LensIconChartMosaic } from '../assets/chart_mosaic'; import { LensIconChartWaffle } from '../assets/chart_waffle'; -import { EMPTY_SIZE_RATIOS } from './constants'; - -import type { SharedPieLayerState } from '../../common/expressions'; -import type { PieChartTypes } from '../../common/expressions/pie_chart/types'; -import type { DatatableColumn } from '../../../../../src/plugins/expressions'; +import { CategoryDisplay, NumberDisplay, SharedPieLayerState, EmptySizeRatios } from '../../common'; +import type { PieChartType } from '../../common/types'; interface PartitionChartMeta { icon: ({ title, titleId, ...props }: Omit) => JSX.Element; label: string; - partitionType: PartitionLayout; groupLabel: string; maxBuckets: number; isExperimental?: boolean; @@ -40,7 +36,7 @@ interface PartitionChartMeta { }>; emptySizeRatioOptions?: Array<{ id: string; - value: EMPTY_SIZE_RATIOS; + value: EmptySizeRatios; label: string; }>; }; @@ -50,10 +46,6 @@ interface PartitionChartMeta { hideNestedLegendSwitch?: boolean; getShowLegendDefault?: (bucketColumns: DatatableColumn[]) => boolean; }; - sortPredicate?: ( - bucketColumns: DatatableColumn[], - sortingMap: Record - ) => (node1: ArrayEntry, node2: ArrayEntry) => number; } const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', { @@ -62,19 +54,19 @@ const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', { const categoryOptions: PartitionChartMeta['toolbarPopover']['categoryOptions'] = [ { - value: 'default', + value: CategoryDisplay.DEFAULT, inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', { defaultMessage: 'Inside or outside', }), }, { - value: 'inside', + value: CategoryDisplay.INSIDE, inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', { defaultMessage: 'Inside only', }), }, { - value: 'hide', + value: CategoryDisplay.HIDE, inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', { defaultMessage: 'Hide labels', }), @@ -83,13 +75,13 @@ const categoryOptions: PartitionChartMeta['toolbarPopover']['categoryOptions'] = const categoryOptionsTreemap: PartitionChartMeta['toolbarPopover']['categoryOptions'] = [ { - value: 'default', + value: CategoryDisplay.DEFAULT, inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', { defaultMessage: 'Show labels', }), }, { - value: 'hide', + value: CategoryDisplay.HIDE, inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', { defaultMessage: 'Hide labels', }), @@ -98,19 +90,19 @@ const categoryOptionsTreemap: PartitionChartMeta['toolbarPopover']['categoryOpti const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [ { - value: 'hidden', + value: NumberDisplay.HIDDEN, inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', { defaultMessage: 'Hide from chart', }), }, { - value: 'percent', + value: NumberDisplay.PERCENT, inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', { defaultMessage: 'Show percent', }), }, { - value: 'value', + value: NumberDisplay.VALUE, inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', { defaultMessage: 'Show value', }), @@ -120,34 +112,33 @@ const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [ const emptySizeRatioOptions: PartitionChartMeta['toolbarPopover']['emptySizeRatioOptions'] = [ { id: 'emptySizeRatioOption-small', - value: EMPTY_SIZE_RATIOS.SMALL, + value: EmptySizeRatios.SMALL, label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.small', { defaultMessage: 'Small', }), }, { id: 'emptySizeRatioOption-medium', - value: EMPTY_SIZE_RATIOS.MEDIUM, + value: EmptySizeRatios.MEDIUM, label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.medium', { defaultMessage: 'Medium', }), }, { id: 'emptySizeRatioOption-large', - value: EMPTY_SIZE_RATIOS.LARGE, + value: EmptySizeRatios.LARGE, label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.large', { defaultMessage: 'Large', }), }, ]; -export const PartitionChartsMeta: Record = { +export const PartitionChartsMeta: Record = { donut: { icon: LensIconChartDonut, label: i18n.translate('xpack.lens.pie.donutLabel', { defaultMessage: 'Donut', }), - partitionType: PartitionLayout.sunburst, groupLabel, maxBuckets: 3, toolbarPopover: { @@ -164,7 +155,6 @@ export const PartitionChartsMeta: Record = { label: i18n.translate('xpack.lens.pie.pielabel', { defaultMessage: 'Pie', }), - partitionType: PartitionLayout.sunburst, groupLabel, maxBuckets: 3, toolbarPopover: { @@ -180,7 +170,6 @@ export const PartitionChartsMeta: Record = { label: i18n.translate('xpack.lens.pie.treemaplabel', { defaultMessage: 'Treemap', }), - partitionType: PartitionLayout.treemap, groupLabel, maxBuckets: 2, toolbarPopover: { @@ -196,7 +185,6 @@ export const PartitionChartsMeta: Record = { label: i18n.translate('xpack.lens.pie.mosaiclabel', { defaultMessage: 'Mosaic', }), - partitionType: PartitionLayout.mosaic, groupLabel, maxBuckets: 2, isExperimental: true, @@ -208,23 +196,12 @@ export const PartitionChartsMeta: Record = { getShowLegendDefault: () => false, }, requiredMinDimensionCount: 2, - sortPredicate: - (bucketColumns, sortingMap) => - ([name1, node1], [, node2]) => { - // Sorting for first group - if (bucketColumns.length === 1 || (node1.children.length && name1 in sortingMap)) { - return sortingMap[name1]; - } - // Sorting for second group - return node2.value - node1.value; - }, }, waffle: { icon: LensIconChartWaffle, label: i18n.translate('xpack.lens.pie.wafflelabel', { defaultMessage: 'Waffle', }), - partitionType: PartitionLayout.waffle, groupLabel, maxBuckets: 1, isExperimental: true, @@ -239,9 +216,5 @@ export const PartitionChartsMeta: Record = { hideNestedLegendSwitch: true, getShowLegendDefault: () => true, }, - sortPredicate: - () => - ([, node1], [, node2]) => - node2.value - node1.value, }, }; diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts index 231b6bacbbe20e8..78f082b8c0e2907 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './expression'; export * from './visualization'; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx deleted file mode 100644 index 8cd8e4f50d62520..000000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { - Partition, - SeriesIdentifier, - Settings, - NodeColorAccessor, - ShapeTreeNode, - HierarchyOfArrays, - Chart, - PartialTheme, -} from '@elastic/charts'; -import { shallow } from 'enzyme'; -import type { LensMultiTable } from '../../common'; -import type { PieExpressionArgs } from '../../common/expressions'; -import { PieComponent } from './render_function'; -import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; -import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { LensIconChartDonut } from '../assets/chart_donut'; - -const chartsThemeService = chartPluginMock.createSetupContract().theme; - -describe('PieVisualization component', () => { - let getFormatSpy: jest.Mock; - let convertSpy: jest.Mock; - - beforeEach(() => { - convertSpy = jest.fn((x) => x); - getFormatSpy = jest.fn(); - getFormatSpy.mockReturnValue({ convert: convertSpy }); - }); - - describe('legend options', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'string' } }, - { id: 'c', name: 'c', meta: { type: 'number' } }, - ], - rows: [ - { a: 6, b: 'I', c: 2, d: 'Row 1' }, - { a: 1, b: 'J', c: 5, d: 'Row 2' }, - ], - }, - }, - }; - - const args: PieExpressionArgs = { - shape: 'pie', - groups: ['a', 'b'], - metric: 'c', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', - legendMaxLines: 1, - truncateLegend: true, - nestedLegend: false, - percentDecimals: 3, - hideLabels: false, - palette: { name: 'mock', type: 'palette' }, - }; - - function getDefaultArgs() { - return { - data, - formatFactory: getFormatSpy, - onClickValue: jest.fn(), - chartsThemeService, - paletteService: chartPluginMock.createPaletteRegistry(), - renderMode: 'view' as const, - syncColors: false, - }; - } - - test('it shows legend on correct side', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('legendPosition')).toEqual('top'); - }); - - test('it shows legend for 2 groups using default legendDisplay', () => { - const component = shallow(); - expect(component.find(Settings).prop('showLegend')).toEqual(true); - }); - - test('it hides legend for 1 group using default legendDisplay', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it hides legend that would show otherwise in preview mode', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it sets the correct lines per legend item', () => { - const component = shallow(); - expect(component.find(Settings).prop('theme')[0]).toMatchObject({ - background: { - color: undefined, - }, - legend: { - labelOptions: { - maxLines: 1, - }, - }, - }); - }); - - test('it calls the color function with the right series layers', () => { - const defaultArgs = getDefaultArgs(); - const component = shallow( - - ); - - (component.find(Partition).prop('layers')![1].shape!.fillColor as NodeColorAccessor)( - { - dataName: 'third', - depth: 2, - parent: { - children: [ - ['first', {}], - ['second', {}], - ['third', {}], - ], - depth: 1, - value: 200, - dataName: 'css', - parent: { - children: [ - ['empty', {}], - ['css', {}], - ['gz', {}], - ], - depth: 0, - sortIndex: 0, - value: 500, - }, - sortIndex: 1, - }, - value: 41, - sortIndex: 2, - } as unknown as ShapeTreeNode, - 0, - [] as HierarchyOfArrays - ); - - expect(defaultArgs.paletteService.get('mock').getCategoricalColor).toHaveBeenCalledWith( - [ - { - name: 'css', - rankAtDepth: 1, - totalSeriesAtDepth: 3, - }, - { - name: 'third', - rankAtDepth: 2, - totalSeriesAtDepth: 3, - }, - ], - { - maxDepth: 2, - totalSeries: 5, - syncColors: false, - behindText: true, - }, - undefined - ); - }); - - test('it hides legend with 2 groups for treemap', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it shows treemap legend only when forced on', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(true); - }); - - test('it defaults to 1-level legend depth', () => { - const component = shallow(); - expect(component.find(Settings).prop('legendMaxDepth')).toEqual(1); - }); - - test('it shows nested legend only when forced on', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined(); - }); - - test('it calls filter callback with the given context', () => { - const defaultArgs = getDefaultArgs(); - const component = shallow(); - component.find(Settings).first().prop('onElementClick')!([ - [ - [ - { - groupByRollup: 6, - value: 6, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - {} as SeriesIdentifier, - ], - ]); - - expect(defaultArgs.onClickValue.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "column": 0, - "row": 0, - "table": Object { - "columns": Array [ - Object { - "id": "a", - "meta": Object { - "type": "number", - }, - "name": "a", - }, - Object { - "id": "b", - "meta": Object { - "type": "string", - }, - "name": "b", - }, - Object { - "id": "c", - "meta": Object { - "type": "number", - }, - "name": "c", - }, - ], - "rows": Array [ - Object { - "a": 6, - "b": "I", - "c": 2, - "d": "Row 1", - }, - Object { - "a": 1, - "b": "J", - "c": 5, - "d": "Row 2", - }, - ], - "type": "datatable", - }, - "value": 6, - }, - ], - } - `); - }); - - test('does not set click listener and legend actions on non-interactive mode', () => { - const defaultArgs = getDefaultArgs(); - const component = shallow( - - ); - expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined(); - expect(component.find(Settings).first().prop('legendAction')).toBeUndefined(); - }); - - test('it renders the empty placeholder when metric contains only falsy data', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: 0, b: 'I', c: 0, d: 'Row 1' }, - { a: 0, b: 'J', c: null, d: 'Row 2' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect(component.find(VisualizationContainer)).toHaveLength(1); - expect(component.find(EmptyPlaceholder)).toHaveLength(1); - }); - - test('it renders the chart when metric contains truthy data and buckets contain only falsy data', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - // a and b are buckets, c is a metric - rows: [{ a: 0, b: undefined, c: 12 }], - }, - }, - }; - - const component = shallow( - - ); - - expect(component.find(VisualizationContainer)).toHaveLength(1); - expect(component.find(EmptyPlaceholder)).toHaveLength(0); - expect(component.find(Chart)).toHaveLength(1); - }); - - test('it shows emptyPlaceholder for undefined grouped data', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: undefined, b: 'I', c: undefined, d: 'Row 1' }, - { a: undefined, b: 'J', c: undefined, d: 'Row 2' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect(component.find(VisualizationContainer)).toHaveLength(1); - expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut); - }); - - test('it should dynamically shrink the chart area to when some small slices are detected', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: 60, b: 'I', c: 200, d: 'Row 1' }, - { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect( - component.find(Settings).prop('theme')[0].partition?.outerSizeRatio - ).toBeCloseTo(1 / 1.05); - }); - - test('it should bound the shrink the chart area to ~20% when some small slices are detected', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: 60, b: 'I', c: 200, d: 'Row 1' }, - { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, - { a: 1, b: 'K', c: 0.1, d: 'Row 3' }, - { a: 1, b: 'G', c: 0.1, d: 'Row 4' }, - { a: 1, b: 'H', c: 0.1, d: 'Row 5' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect( - component.find(Settings).prop('theme')[0].partition?.outerSizeRatio - ).toBeCloseTo(1 / 1.2); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx deleted file mode 100644 index 15706e69d1e1665..000000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uniq } from 'lodash'; -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { Required } from '@kbn/utility-types'; -import { EuiText } from '@elastic/eui'; -import { - Chart, - Datum, - LayerValue, - Partition, - PartitionLayer, - Position, - Settings, - ElementClickListener, - PartialTheme, -} from '@elastic/charts'; -import { RenderMode } from 'src/plugins/expressions'; -import type { LensFilterEvent } from '../types'; -import { VisualizationContainer } from '../visualization_container'; -import { DEFAULT_PERCENT_DECIMALS } from './constants'; -import { PartitionChartsMeta } from './partition_charts_meta'; -import type { FormatFactory } from '../../common'; -import type { PieExpressionProps } from '../../common/expressions'; -import { - getSliceValue, - getFilterContext, - isTreemapOrMosaicShape, - byDataColorPaletteMap, - extractUniqTermsMap, -} from './render_helpers'; -import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; -import './visualization.scss'; -import { - ChartsPluginSetup, - PaletteRegistry, - SeriesLayer, -} from '../../../../../src/plugins/charts/public'; -import { LensIconChartDonut } from '../assets/chart_donut'; -import { getLegendAction } from './get_legend_action'; - -declare global { - interface Window { - /** - * Flag used to enable debugState on elastic charts - */ - _echDebugStateFlag?: boolean; - } -} - -const EMPTY_SLICE = Symbol('empty_slice'); - -export function PieComponent( - props: PieExpressionProps & { - formatFactory: FormatFactory; - chartsThemeService: ChartsPluginSetup['theme']; - interactive?: boolean; - paletteService: PaletteRegistry; - onClickValue: (data: LensFilterEvent['data']) => void; - renderMode: RenderMode; - syncColors: boolean; - } -) { - const [firstTable] = Object.values(props.data.tables); - const formatters: Record> = {}; - - const { chartsThemeService, paletteService, syncColors, onClickValue } = props; - const { - shape, - groups, - metric, - numberDisplay, - categoryDisplay, - legendDisplay, - legendPosition, - nestedLegend, - percentDecimals, - emptySizeRatio, - legendMaxLines, - truncateLegend, - hideLabels, - palette, - showValuesInLegend, - } = props.args; - const chartTheme = chartsThemeService.useChartsTheme(); - const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); - const isDarkMode = chartsThemeService.useDarkMode(); - - if (!hideLabels) { - firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.meta.params); - }); - } - - const fillLabel: PartitionLayer['fillLabel'] = { - valueFont: { - fontWeight: 700, - }, - }; - - if (numberDisplay === 'hidden') { - // Hides numbers from appearing inside chart, but they still appear in linkLabel - // and tooltips. - fillLabel.valueFormatter = () => ''; - } - - const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id)); - const totalSeriesCount = uniq( - firstTable.rows.map((row) => { - return bucketColumns.map(({ id: columnId }) => row[columnId]).join(','); - }) - ).length; - - const shouldUseByDataPalette = !syncColors && ['mosaic'].includes(shape) && bucketColumns[1]?.id; - let byDataPalette: ReturnType; - if (shouldUseByDataPalette) { - byDataPalette = byDataColorPaletteMap( - firstTable, - bucketColumns[1].id, - paletteService.get(palette.name), - palette - ); - } - - let sortingMap: Record = {}; - if (shape === 'mosaic') { - sortingMap = extractUniqTermsMap(firstTable, bucketColumns[0].id); - } - - const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => { - return { - groupByRollup: (d: Datum) => d[col.id] ?? EMPTY_SLICE, - showAccessor: (d: Datum) => d !== EMPTY_SLICE, - nodeLabel: (d: unknown) => { - if (hideLabels || d === EMPTY_SLICE) { - return ''; - } - if (col.meta.params) { - return formatters[col.id].convert(d) ?? ''; - } - return String(d); - }, - fillLabel, - sortPredicate: PartitionChartsMeta[shape].sortPredicate?.(bucketColumns, sortingMap), - shape: { - fillColor: (d) => { - const seriesLayers: SeriesLayer[] = []; - - // Mind the difference here: the contrast computation for the text ignores the alpha/opacity - // therefore change it for dask mode - const defaultColor = isDarkMode ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)'; - - // Color is determined by round-robin on the index of the innermost slice - // This has to be done recursively until we get to the slice index - let tempParent: typeof d | typeof d['parent'] = d; - - while (tempParent.parent && tempParent.depth > 0) { - seriesLayers.unshift({ - name: String(tempParent.parent.children[tempParent.sortIndex][0]), - rankAtDepth: tempParent.sortIndex, - totalSeriesAtDepth: tempParent.parent.children.length, - }); - tempParent = tempParent.parent; - } - - if (byDataPalette && seriesLayers[1]) { - return byDataPalette.getColor(seriesLayers[1].name) || defaultColor; - } - - if (isTreemapOrMosaicShape(shape)) { - // Only highlight the innermost color of the treemap, as it accurately represents area - if (layerIndex < bucketColumns.length - 1) { - return defaultColor; - } - // only use the top level series layer for coloring - if (seriesLayers.length > 1) { - seriesLayers.pop(); - } - } - - const outputColor = paletteService.get(palette.name).getCategoricalColor( - seriesLayers, - { - behindText: categoryDisplay !== 'hide' || isTreemapOrMosaicShape(shape), - maxDepth: bucketColumns.length, - totalSeries: totalSeriesCount, - syncColors, - }, - palette.params - ); - - return outputColor || defaultColor; - }, - }, - }; - }); - - const { legend, partitionType, label: chartType } = PartitionChartsMeta[shape]; - - const themeOverrides: Required = { - chartMargins: { top: 0, bottom: 0, left: 0, right: 0 }, - background: { - color: undefined, // removes background for embeddables - }, - legend: { - labelOptions: { maxLines: truncateLegend ? legendMaxLines ?? 1 : 0 }, - }, - partition: { - fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily, - outerSizeRatio: 1, - minFontSize: 10, - maxFontSize: 16, - // Labels are added outside the outer ring when the slice is too small - linkLabel: { - maxCount: 5, - fontSize: 11, - // Dashboard background color is affected by dark mode, which we need - // to account for in outer labels - // This does not handle non-dashboard embeddables, which are allowed to - // have different backgrounds. - textColor: chartTheme.axes?.axisTitle?.fill, - }, - sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, - sectorLineWidth: 1.5, - circlePadding: 4, - }, - }; - if (isTreemapOrMosaicShape(shape)) { - if (hideLabels || categoryDisplay === 'hide') { - themeOverrides.partition.fillLabel = { textColor: 'rgba(0,0,0,0)' }; - } - } else { - themeOverrides.partition.emptySizeRatio = shape === 'donut' ? emptySizeRatio : 0; - - if (hideLabels || categoryDisplay === 'hide') { - // Force all labels to be linked, then prevent links from showing - themeOverrides.partition.linkLabel = { - maxCount: 0, - maximumSection: Number.POSITIVE_INFINITY, - }; - } else if (categoryDisplay === 'inside') { - // Prevent links from showing - themeOverrides.partition.linkLabel = { maxCount: 0 }; - } else { - // if it contains any slice below 2% reduce the ratio - // first step: sum it up the overall sum - const overallSum = firstTable.rows.reduce((sum, row) => sum + row[metric!], 0); - const slices = firstTable.rows.map((row) => row[metric!] / overallSum); - const smallSlices = slices.filter((value) => value < 0.02).length; - if (smallSlices) { - // shrink up to 20% to give some room for the linked values - themeOverrides.partition.outerSizeRatio = 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); - } - } - } - const metricColumn = firstTable.columns.find((c) => c.id === metric)!; - const percentFormatter = props.formatFactory({ - id: 'percent', - params: { - pattern: `0,0.[${'0'.repeat(percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}]%`, - }, - }); - - const hasNegative = firstTable.rows.some((row) => { - const value = row[metricColumn.id]; - return typeof value === 'number' && value < 0; - }); - - const isMetricEmpty = firstTable.rows.every((row) => { - return !row[metricColumn.id]; - }); - - const isEmpty = - firstTable.rows.length === 0 || - firstTable.rows.every((row) => groups.every((colId) => typeof row[colId] === 'undefined')) || - isMetricEmpty; - - if (isEmpty) { - return ( - - - - ); - } - - if (hasNegative) { - return ( - - - - ); - } - - const onElementClickHandler: ElementClickListener = (args) => { - const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable); - - onClickValue(context); - }; - - return ( - - - - - ); -} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index bcd9d79babbab2a..bf09b3f2706e5b6 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -6,321 +6,11 @@ */ import type { Datatable } from 'src/plugins/expressions/public'; -import type { PaletteDefinition, PaletteOutput } from 'src/plugins/charts/public'; -import { - getSliceValue, - getFilterContext, - byDataColorPaletteMap, - extractUniqTermsMap, - checkTableForContainsSmallValues, - shouldShowValuesInLegend, -} from './render_helpers'; -import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import type { PieLayerState } from '../../common/expressions'; +import { checkTableForContainsSmallValues, shouldShowValuesInLegend } from './render_helpers'; +import { PieLayerState, PieChartTypes } from '../../common'; describe('render helpers', () => { - describe('#getSliceValue', () => { - it('returns the metric when positive number', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: 5 }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(5); - }); - - it('returns the metric when negative number', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: -100 }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(0); - }); - - it('returns 0 when metric value is 0', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: 0 }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(0); - }); - - it('returns 0 when metric value is infinite', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: Number.POSITIVE_INFINITY }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(0); - }); - }); - - describe('#getFilterContext', () => { - it('handles single slice click for single ring', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 2 }, - { a: 'Test', b: 4 }, - { a: 'Foo', b: 6 }, - ], - }; - expect( - getFilterContext( - [ - { - groupByRollup: 'Test', - value: 100, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - ['a'], - table - ) - ).toEqual({ - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - ], - }); - }); - - it('handles single slice click with 2 rings', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'string' } }, - { id: 'c', name: 'C', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 'Two', c: 2 }, - { a: 'Test', b: 'Two', c: 5 }, - { a: 'Foo', b: 'Three', c: 6 }, - ], - }; - expect( - getFilterContext( - [ - { - groupByRollup: 'Test', - value: 100, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - ['a', 'b'], - table - ) - ).toEqual({ - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - ], - }); - }); - - it('finds right row for multi slice click', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'string' } }, - { id: 'c', name: 'C', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 'Two', c: 2 }, - { a: 'Test', b: 'Two', c: 5 }, - { a: 'Foo', b: 'Three', c: 6 }, - ], - }; - expect( - getFilterContext( - [ - { - groupByRollup: 'Test', - value: 100, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - { - groupByRollup: 'Two', - value: 5, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - ['a', 'b'], - table - ) - ).toEqual({ - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - { - row: 1, - column: 1, - value: 'Two', - table, - }, - ], - }); - }); - }); - - describe('#extractUniqTermsMap', () => { - it('should extract map', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'string' } }, - { id: 'c', name: 'C', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 'Two', c: 2 }, - { a: 'Test', b: 'Two', c: 5 }, - { a: 'Foo', b: 'Three', c: 6 }, - ], - }; - expect(extractUniqTermsMap(table, 'a')).toMatchInlineSnapshot(` - Object { - "Foo": 2, - "Hi": 0, - "Test": 1, - } - `); - expect(extractUniqTermsMap(table, 'b')).toMatchInlineSnapshot(` - Object { - "Three": 1, - "Two": 0, - } - `); - }); - }); - - describe('#byDataColorPaletteMap', () => { - let datatable: Datatable; - let paletteDefinition: PaletteDefinition; - let palette: PaletteOutput; - const columnId = 'foo'; - - beforeEach(() => { - datatable = { - rows: [ - { - [columnId]: '1', - }, - { - [columnId]: '2', - }, - ], - } as unknown as Datatable; - paletteDefinition = chartPluginMock.createPaletteRegistry().get('default'); - palette = { type: 'palette' } as PaletteOutput; - }); - - it('should create byDataColorPaletteMap', () => { - expect(byDataColorPaletteMap(datatable, columnId, paletteDefinition, palette)) - .toMatchInlineSnapshot(` - Object { - "getColor": [Function], - } - `); - }); - - it('should get color', () => { - const colorPaletteMap = byDataColorPaletteMap( - datatable, - columnId, - paletteDefinition, - palette - ); - - expect(colorPaletteMap.getColor('1')).toBe('black'); - }); - - it('should return undefined in case if values not in datatable', () => { - const colorPaletteMap = byDataColorPaletteMap( - datatable, - columnId, - paletteDefinition, - palette - ); - - expect(colorPaletteMap.getColor('wrong')).toBeUndefined(); - }); - - it('should increase rankAtDepth for each new value', () => { - const colorPaletteMap = byDataColorPaletteMap( - datatable, - columnId, - paletteDefinition, - palette - ); - colorPaletteMap.getColor('1'); - colorPaletteMap.getColor('2'); - - expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( - 1, - [{ name: '1', rankAtDepth: 0, totalSeriesAtDepth: 2 }], - { behindText: false }, - undefined - ); - - expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( - 2, - [{ name: '2', rankAtDepth: 1, totalSeriesAtDepth: 2 }], - { behindText: false }, - undefined - ); - }); - }); - describe('#checkTableForContainsSmallValues', () => { let datatable: Datatable; const columnId = 'foo'; @@ -380,23 +70,35 @@ describe('render helpers', () => { describe('#shouldShowValuesInLegend', () => { it('should firstly read the state value', () => { expect( - shouldShowValuesInLegend({ showValuesInLegend: true } as PieLayerState, 'waffle') + shouldShowValuesInLegend( + { showValuesInLegend: true } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toBeTruthy(); expect( - shouldShowValuesInLegend({ showValuesInLegend: false } as PieLayerState, 'waffle') + shouldShowValuesInLegend( + { showValuesInLegend: false } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toBeFalsy(); }); it('should read value from meta in case of value in state is undefined', () => { expect( - shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'waffle') + shouldShowValuesInLegend( + { showValuesInLegend: undefined } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toBeTruthy(); - expect(shouldShowValuesInLegend({} as PieLayerState, 'waffle')).toBeTruthy(); + expect(shouldShowValuesInLegend({} as PieLayerState, PieChartTypes.WAFFLE)).toBeTruthy(); expect( - shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'pie') + shouldShowValuesInLegend( + { showValuesInLegend: undefined } as PieLayerState, + PieChartTypes.PIE + ) ).toBeFalsy(); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index a9685e13e17741d..1f6d40abc32ec2f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -5,47 +5,14 @@ * 2.0. */ -import type { Datum, LayerValue } from '@elastic/charts'; -import type { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; -import type { LensFilterEvent } from '../types'; -import type { PieChartTypes, PieLayerState } from '../../common/expressions/pie_chart/types'; -import type { PaletteDefinition, PaletteOutput } from '../../../../../src/plugins/charts/public'; +import type { Datatable } from 'src/plugins/expressions/public'; +import type { PieChartType, PieLayerState } from '../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; -export function getSliceValue(d: Datum, metricColumn: DatatableColumn) { - const value = d[metricColumn.id]; - return Number.isFinite(value) && value >= 0 ? value : 0; -} - -export function getFilterContext( - clickedLayers: LayerValue[], - layerColumnIds: string[], - table: Datatable -): LensFilterEvent['data'] { - const matchingIndex = table.rows.findIndex((row) => - clickedLayers.every((layer, index) => { - const columnId = layerColumnIds[index]; - return row[columnId] === layer.groupByRollup; - }) - ); - - return { - data: clickedLayers.map((clickedLayer, index) => ({ - column: table.columns.findIndex((col) => col.id === layerColumnIds[index]), - row: matchingIndex, - value: clickedLayer.groupByRollup, - table, - })), - }; -} - -export const isPartitionShape = (shape: PieChartTypes | string) => +export const isPartitionShape = (shape: PieChartType | string) => ['donut', 'pie', 'treemap', 'mosaic', 'waffle'].includes(shape); -export const isTreemapOrMosaicShape = (shape: PieChartTypes | string) => - ['treemap', 'mosaic'].includes(shape); - -export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTypes) => { +export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartType) => { if ('showValues' in PartitionChartsMeta[shape]?.legend) { return layer.showValuesInLegend ?? PartitionChartsMeta[shape]?.legend?.showValues ?? true; } @@ -53,58 +20,6 @@ export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTy return false; }; -export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) => - [...new Set(dataTable.rows.map((item) => item[columnId]))].reduce( - (acc, item, index) => ({ - ...acc, - [item]: index, - }), - {} - ); - -export const byDataColorPaletteMap = ( - dataTable: Datatable, - columnId: string, - paletteDefinition: PaletteDefinition, - { params }: PaletteOutput -) => { - const colorMap = new Map( - dataTable.rows.map((item) => [String(item[columnId]), undefined]) - ); - let rankAtDepth = 0; - - return { - getColor: (item: unknown) => { - const key = String(item); - - if (colorMap.has(key)) { - let color = colorMap.get(key); - - if (color) { - return color; - } - color = - paletteDefinition.getCategoricalColor( - [ - { - name: key, - totalSeriesAtDepth: colorMap.size, - rankAtDepth: rankAtDepth++, - }, - ], - { - behindText: false, - }, - params - ) || undefined; - - colorMap.set(key, color); - return color; - } - }, - }; -}; - export const checkTableForContainsSmallValues = ( dataTable: Datatable, columnId: string, diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index 229ef9b387ac09f..f951d4f07e86581 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -8,7 +8,14 @@ import { PaletteOutput } from 'src/plugins/charts/public'; import { suggestions } from './suggestions'; import type { DataType, SuggestionRequest } from '../types'; -import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; +import { + CategoryDisplay, + LegendDisplay, + NumberDisplay, + PieChartTypes, + PieLayerState, + PieVisualizationState, +} from '../../common'; import { layerTypes } from '../../common'; describe('suggestions', () => { @@ -53,16 +60,16 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -168,7 +175,7 @@ describe('suggestions', () => { changeType: 'initial', }, state: { - shape: 'mosaic', + shape: PieChartTypes.MOSAIC, layers: [{} as PieLayerState], }, keptLayerIds: ['first'], @@ -380,7 +387,7 @@ describe('suggestions', () => { expect(results).toContainEqual( expect.objectContaining({ - state: expect.objectContaining({ shape: 'donut' }), + state: expect.objectContaining({ shape: PieChartTypes.DONUT }), }) ); }); @@ -412,7 +419,7 @@ describe('suggestions', () => { expect(results).toContainEqual( expect.objectContaining({ - state: expect.objectContaining({ shape: 'pie' }), + state: expect.objectContaining({ shape: PieChartTypes.PIE }), }) ); }); @@ -542,7 +549,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, palette, layers: [ { @@ -551,9 +558,9 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, @@ -566,7 +573,7 @@ describe('suggestions', () => { ).toContainEqual( expect.objectContaining({ state: { - shape: 'donut', + shape: PieChartTypes.DONUT, palette, layers: [ { @@ -575,8 +582,8 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'inside', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, legendDisplay: 'show', percentDecimals: 0, legendMaxLines: 1, @@ -601,7 +608,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', @@ -609,9 +616,9 @@ describe('suggestions', () => { groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -651,16 +658,16 @@ describe('suggestions', () => { changeType: 'extended', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'e', - numberDisplay: 'value', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.VALUE, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -700,16 +707,16 @@ describe('suggestions', () => { changeType: 'initial', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'e', - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -737,7 +744,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: 'first', @@ -745,9 +752,9 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, @@ -760,7 +767,7 @@ describe('suggestions', () => { ).toContainEqual( expect.objectContaining({ state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', @@ -768,8 +775,8 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'default', // This is changed + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, // This is changed legendDisplay: 'show', percentDecimals: 0, legendMaxLines: 1, @@ -794,7 +801,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'mosaic', + shape: PieChartTypes.MOSAIC, layers: [ { layerId: 'first', @@ -802,9 +809,9 @@ describe('suggestions', () => { groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -836,7 +843,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', @@ -844,9 +851,9 @@ describe('suggestions', () => { groups: ['a', 'b'], metric: 'c', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, @@ -871,7 +878,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'waffle', + shape: PieChartTypes.WAFFLE, layers: [ { layerId: 'first', @@ -879,9 +886,9 @@ describe('suggestions', () => { groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -909,16 +916,16 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'c', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index dd42dd6474e0b59..0ff75ee823d4275 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -8,11 +8,17 @@ import { partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import type { SuggestionRequest, TableSuggestionColumn, VisualizationSuggestion } from '../types'; -import { layerTypes } from '../../common'; -import type { PieVisualizationState } from '../../common/expressions'; +import { + CategoryDisplay, + layerTypes, + LegendDisplay, + NumberDisplay, + PieChartTypes, + PieVisualizationState, +} from '../../common'; +import type { PieChartType } from '../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; import { isPartitionShape } from './render_helpers'; -import { PieChartTypes } from '../../common/expressions/pie_chart/types'; function hasIntervalScale(columns: TableSuggestionColumn[]) { return columns.some((col) => col.operation.scale === 'interval'); @@ -43,14 +49,19 @@ function getNewShape( let newShape: PieVisualizationState['shape'] | undefined; if (groups.length !== 1 && !subVisualizationId) { - newShape = 'pie'; + newShape = PieChartTypes.PIE; } - return newShape ?? 'donut'; + return newShape ?? PieChartTypes.DONUT; } -function hasCustomSuggestionsExists(shape: PieChartTypes | string | undefined) { - return shape ? ['treemap', 'waffle', 'mosaic'].includes(shape) : false; +function hasCustomSuggestionsExists(shape: PieChartType | string | undefined) { + const shapes: Array = [ + PieChartTypes.TREEMAP, + PieChartTypes.WAFFLE, + PieChartTypes.MOSAIC, + ]; + return shape ? shapes.includes(shape) : false; } const maximumGroupLength = Math.max( @@ -116,9 +127,9 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, @@ -137,13 +148,18 @@ export function suggestions({ ...baseSuggestion, title: i18n.translate('xpack.lens.pie.suggestionLabel', { defaultMessage: 'As {chartName}', - values: { chartName: PartitionChartsMeta[newShape === 'pie' ? 'donut' : 'pie'].label }, + values: { + chartName: + PartitionChartsMeta[ + newShape === PieChartTypes.PIE ? PieChartTypes.DONUT : PieChartTypes.PIE + ].label, + }, description: 'chartName is already translated', }), score: 0.1, state: { ...baseSuggestion.state, - shape: newShape === 'pie' ? 'donut' : 'pie', + shape: newShape === PieChartTypes.PIE ? PieChartTypes.DONUT : PieChartTypes.PIE, }, hide: true, }); @@ -159,9 +175,9 @@ export function suggestions({ }), // Use a higher score when currently active, to prevent chart type switching // on the user unintentionally - score: state?.shape === 'treemap' ? 0.7 : 0.5, + score: state?.shape === PieChartTypes.TREEMAP ? 0.7 : 0.5, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, palette: mainPalette || state?.palette, layers: [ state?.layers[0] @@ -171,8 +187,8 @@ export function suggestions({ groups: groups.map((col) => col.columnId), metric: metricColumnId, categoryDisplay: - state.layers[0].categoryDisplay === 'inside' - ? 'default' + state.layers[0].categoryDisplay === CategoryDisplay.INSIDE + ? CategoryDisplay.DEFAULT : state.layers[0].categoryDisplay, layerType: layerTypes.DATA, } @@ -180,9 +196,9 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, @@ -194,21 +210,21 @@ export function suggestions({ table.changeType === 'reduced' || !state || hasIntervalScale(groups) || - (state && state.shape === 'treemap'), + (state && state.shape === PieChartTypes.TREEMAP), }); } if ( groups.length <= PartitionChartsMeta.mosaic.maxBuckets && - (!subVisualizationId || subVisualizationId === 'mosaic') + (!subVisualizationId || subVisualizationId === PieChartTypes.MOSAIC) ) { results.push({ title: i18n.translate('xpack.lens.pie.mosaicSuggestionLabel', { defaultMessage: 'As Mosaic', }), - score: state?.shape === 'mosaic' ? 0.7 : 0.5, + score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5, state: { - shape: 'mosaic', + shape: PieChartTypes.MOSAIC, palette: mainPalette || state?.palette, layers: [ state?.layers[0] @@ -217,16 +233,16 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - categoryDisplay: 'default', + categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, } : { layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, @@ -239,15 +255,15 @@ export function suggestions({ if ( groups.length <= PartitionChartsMeta.waffle.maxBuckets && - (!subVisualizationId || subVisualizationId === 'waffle') + (!subVisualizationId || subVisualizationId === PieChartTypes.WAFFLE) ) { results.push({ title: i18n.translate('xpack.lens.pie.waffleSuggestionLabel', { defaultMessage: 'As Waffle', }), - score: state?.shape === 'waffle' ? 0.7 : 0.5, + score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.5, state: { - shape: 'waffle', + shape: PieChartTypes.WAFFLE, palette: mainPalette || state?.palette, layers: [ state?.layers[0] @@ -256,16 +272,16 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - categoryDisplay: 'default', + categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, } : { layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index f703b1b5f419bf3..9ae9f4ac0cae4eb 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -6,13 +6,62 @@ */ import type { Ast } from '@kbn/interpreter'; -import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { Position } from '@elastic/charts'; + +import type { PaletteOutput, PaletteRegistry } from '../../../../../src/plugins/charts/public'; +import { + buildExpression, + buildExpressionFunction, +} from '../../../../../src/plugins/expressions/public'; import type { Operation, DatasourcePublicAPI } from '../types'; -import { DEFAULT_PERCENT_DECIMALS, EMPTY_SIZE_RATIOS } from './constants'; +import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { shouldShowValuesInLegend } from './render_helpers'; -import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; +import { + CategoryDisplay, + NumberDisplay, + PieChartTypes, + PieLayerState, + PieVisualizationState, + EmptySizeRatios, + LegendDisplay, +} from '../../common'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; +interface Attributes { + isPreview: boolean; + title?: string; + description?: string; +} + +interface OperationColumnId { + columnId: string; + operation: Operation; +} + +type GenerateExpressionAstFunction = ( + state: PieVisualizationState, + attributes: Attributes, + operations: OperationColumnId[], + layer: PieLayerState, + datasourceLayers: Record, + paletteService: PaletteRegistry +) => Ast | null; + +type GenerateExpressionAstArguments = ( + state: PieVisualizationState, + attributes: Attributes, + operations: OperationColumnId[], + layer: PieLayerState, + datasourceLayers: Record, + paletteService: PaletteRegistry +) => Ast['chain'][number]['arguments']; + +type GenerateLabelsAstArguments = ( + state: PieVisualizationState, + attributes: Attributes, + layer: PieLayerState +) => [Ast]; + export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayerState) => { const originalOrder = datasource .getTableSpec() @@ -22,23 +71,183 @@ export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayer return Array.from(new Set(originalOrder.concat(layer.groups))); }; -export function toExpression( - state: PieVisualizationState, - datasourceLayers: Record, +const prepareDimension = (accessor: string) => { + const visdimension = buildExpressionFunction('visdimension', { accessor }); + return buildExpression([visdimension]).toAst(); +}; + +const generateCommonLabelsAstArgs: GenerateLabelsAstArguments = (state, attributes, layer) => { + const show = [!attributes.isPreview && layer.categoryDisplay !== CategoryDisplay.HIDE]; + const position = layer.categoryDisplay !== CategoryDisplay.HIDE ? [layer.categoryDisplay] : []; + const values = [layer.numberDisplay !== NumberDisplay.HIDDEN]; + const valuesFormat = layer.numberDisplay !== NumberDisplay.HIDDEN ? [layer.numberDisplay] : []; + const percentDecimals = [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS]; + + return [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'partitionLabels', + arguments: { show, position, values, valuesFormat, percentDecimals }, + }, + ], + }, + ]; +}; + +const generateWaffleLabelsAstArguments: GenerateLabelsAstArguments = (...args) => { + const [labelsExpr] = generateCommonLabelsAstArgs(...args); + const [labels] = labelsExpr.chain; + return [ + { + ...labelsExpr, + chain: [{ ...labels, percentDecimals: DEFAULT_PERCENT_DECIMALS }], + }, + ]; +}; + +const generatePaletteAstArguments = ( paletteService: PaletteRegistry, - attributes: Partial<{ title: string; description: string }> = {} -) { - return expressionHelper(state, datasourceLayers, paletteService, { - ...attributes, - isPreview: false, - }); -} + palette?: PaletteOutput +): [Ast] => + palette + ? [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'theme', + arguments: { + variable: ['palette'], + default: [paletteService.get(palette.name).toExpression(palette.params)], + }, + }, + ], + }, + ] + : [paletteService.get('default').toExpression()]; + +const generateCommonArguments: GenerateExpressionAstArguments = ( + state, + attributes, + operations, + layer, + datasourceLayers, + paletteService +) => ({ + labels: generateCommonLabelsAstArgs(state, attributes, layer), + buckets: operations.map((o) => o.columnId).map(prepareDimension), + metric: layer.metric ? [prepareDimension(layer.metric)] : [], + legendDisplay: [attributes.isPreview ? LegendDisplay.HIDE : layer.legendDisplay], + legendPosition: [layer.legendPosition || Position.Right], + maxLegendLines: [layer.legendMaxLines ?? 1], + nestedLegend: [!!layer.nestedLegend], + truncateLegend: [ + layer.truncateLegend ?? getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText, + ], + palette: generatePaletteAstArguments(paletteService, state.palette), +}); + +const generatePieVisAst: GenerateExpressionAstFunction = (...rest) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'pieVis', + arguments: { + ...generateCommonArguments(...rest), + respectSourceOrder: [false], + startFromSecondLargestSlice: [true], + }, + }, + ], +}); + +const generateDonutVisAst: GenerateExpressionAstFunction = (...rest) => { + const [, , , layer] = rest; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'pieVis', + arguments: { + ...generateCommonArguments(...rest), + respectSourceOrder: [false], + isDonut: [true], + startFromSecondLargestSlice: [true], + emptySizeRatio: [layer.emptySizeRatio ?? EmptySizeRatios.SMALL], + }, + }, + ], + }; +}; + +const generateTreemapVisAst: GenerateExpressionAstFunction = (...rest) => { + const [, , , layer] = rest; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'treemapVis', + arguments: { + ...generateCommonArguments(...rest), + nestedLegend: [!!layer.nestedLegend], + }, + }, + ], + }; +}; + +const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'mosaicVis', + arguments: generateCommonArguments(...rest), + }, + ], +}); + +const generateWaffleVisAst: GenerateExpressionAstFunction = (...rest) => { + const { buckets, nestedLegend, ...args } = generateCommonArguments(...rest); + const [state, attributes, , layer] = rest; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'waffleVis', + arguments: { + ...args, + bucket: buckets, + labels: generateWaffleLabelsAstArguments(state, attributes, layer), + showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], + }, + }, + ], + }; +}; + +const generateExprAst: GenerateExpressionAstFunction = (state, ...restArgs) => + ({ + [PieChartTypes.PIE]: () => generatePieVisAst(state, ...restArgs), + [PieChartTypes.DONUT]: () => generateDonutVisAst(state, ...restArgs), + [PieChartTypes.TREEMAP]: () => generateTreemapVisAst(state, ...restArgs), + [PieChartTypes.MOSAIC]: () => generateMosaicVisAst(state, ...restArgs), + [PieChartTypes.WAFFLE]: () => generateWaffleVisAst(state, ...restArgs), + }[state.shape]()); function expressionHelper( state: PieVisualizationState, datasourceLayers: Record, paletteService: PaletteRegistry, - attributes: { isPreview: boolean; title?: string; description?: string } = { isPreview: false } + attributes: Attributes = { isPreview: false } ): Ast | null { const layer = state.layers[0]; const datasource = datasourceLayers[layer.layerId]; @@ -51,63 +260,20 @@ function expressionHelper( if (!layer.metric || !operations.length) { return null; } - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_pie', - arguments: { - title: [attributes.title || ''], - description: [attributes.description || ''], - shape: [state.shape], - hideLabels: [attributes.isPreview], - groups: operations.map((o) => o.columnId), - metric: [layer.metric], - numberDisplay: [layer.numberDisplay], - categoryDisplay: [layer.categoryDisplay], - legendDisplay: [layer.legendDisplay], - legendPosition: [layer.legendPosition || 'right'], - emptySizeRatio: [layer.emptySizeRatio ?? EMPTY_SIZE_RATIOS.SMALL], - showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], - percentDecimals: [ - state.shape === 'waffle' - ? DEFAULT_PERCENT_DECIMALS - : layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS, - ], - legendMaxLines: [layer.legendMaxLines ?? 1], - truncateLegend: [ - layer.truncateLegend ?? - getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText, - ], - nestedLegend: [!!layer.nestedLegend], - ...(state.palette - ? { - palette: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'theme', - arguments: { - variable: ['palette'], - default: [ - paletteService - .get(state.palette.name) - .toExpression(state.palette.params), - ], - }, - }, - ], - }, - ], - } - : {}), - }, - }, - ], - }; + + return generateExprAst(state, attributes, operations, layer, datasourceLayers, paletteService); +} + +export function toExpression( + state: PieVisualizationState, + datasourceLayers: Record, + paletteService: PaletteRegistry, + attributes: Partial<{ title: string; description: string }> = {} +) { + return expressionHelper(state, datasourceLayers, paletteService, { + ...attributes, + isPreview: false, + }); } export function toPreviewExpression( diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index cebacd5c95863a0..f188aa12069d75f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -20,7 +20,7 @@ import type { Position } from '@elastic/charts'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { PartitionChartsMeta } from './partition_charts_meta'; -import type { PieVisualizationState, SharedPieLayerState } from '../../common/expressions'; +import { LegendDisplay, PieVisualizationState, SharedPieLayerState } from '../../common'; import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types'; import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components'; import { PalettePicker } from '../shared_components'; @@ -34,21 +34,21 @@ const legendOptions: Array<{ }> = [ { id: 'pieLegendDisplay-default', - value: 'default', + value: LegendDisplay.DEFAULT, label: i18n.translate('xpack.lens.pieChart.legendVisibility.auto', { defaultMessage: 'Auto', }), }, { id: 'pieLegendDisplay-show', - value: 'show', + value: LegendDisplay.SHOW, label: i18n.translate('xpack.lens.pieChart.legendVisibility.show', { defaultMessage: 'Show', }), }, { id: 'pieLegendDisplay-hide', - value: 'hide', + value: LegendDisplay.HIDE, label: i18n.translate('xpack.lens.pieChart.legendVisibility.hide', { defaultMessage: 'Hide', }), diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.scss b/x-pack/plugins/lens/public/pie_visualization/visualization.scss deleted file mode 100644 index a8890208596b6cc..000000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.scss +++ /dev/null @@ -1,7 +0,0 @@ -.lnsPieExpression__container { - height: 100%; - width: 100%; - // the FocusTrap is adding extra divs which are making the visualization redraw twice - // with a visible glitch. This make the chart library resilient to this extra reflow - overflow-x: hidden; -} diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts index 86ac635e36068d6..c178613657947bc 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts @@ -6,7 +6,13 @@ */ import { getPieVisualization } from './visualization'; -import type { PieVisualizationState } from '../../common/expressions'; +import { + PieVisualizationState, + PieChartTypes, + CategoryDisplay, + NumberDisplay, + LegendDisplay, +} from '../../common'; import { layerTypes } from '../../common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; @@ -24,16 +30,16 @@ const pieVisualization = getPieVisualization({ function getExampleState(): PieVisualizationState { return { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: LAYER_ID, layerType: layerTypes.DATA, groups: [], metric: undefined, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, }, ], @@ -81,14 +87,14 @@ describe('pie_visualization', () => { groups: ['a'], layerId: LAYER_ID, layerType: layerTypes.DATA, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, metric: undefined, }, ], - shape: 'donut', + shape: PieChartTypes.DONUT, }; const setDimensionResult = pieVisualization.setDimension({ prevState, @@ -100,7 +106,7 @@ describe('pie_visualization', () => { expect(setDimensionResult).toEqual( expect.objectContaining({ - shape: 'donut', + shape: PieChartTypes.DONUT, }) ); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 8c52fc5a52fd8ce..0e8f05eff892051 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -20,21 +20,21 @@ import type { VisualizationDimensionGroupConfig, } from '../types'; import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression'; -import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; -import { layerTypes } from '../../common'; +import { CategoryDisplay, layerTypes, LegendDisplay, NumberDisplay } from '../../common'; import { suggestions } from './suggestions'; import { PartitionChartsMeta } from './partition_charts_meta'; import { DimensionEditor, PieToolbar } from './toolbar'; import { checkTableForContainsSmallValues } from './render_helpers'; +import { PieChartTypes, PieLayerState, PieVisualizationState } from '../../common'; function newLayerState(layerId: string): PieLayerState { return { layerId, groups: [], metric: undefined, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }; @@ -108,7 +108,7 @@ export const getPieVisualization = ({ initialize(addNewLayer, state, mainPalette) { return ( state || { - shape: 'donut', + shape: PieChartTypes.DONUT, layers: [newLayerState(addNewLayer())], palette: mainPalette, } diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts index a04ad27d1a27618..f258db7f9aedee0 100644 --- a/x-pack/plugins/lens/server/expressions/expressions.ts +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -7,7 +7,6 @@ import type { CoreSetup } from 'kibana/server'; import { - pie, xyChart, counterRate, metricChart, @@ -36,7 +35,6 @@ export const setupExpressions = ( [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); [ - pie, xyChart, counterRate, metricChart, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts index 381528e1055d634..8ea7ff07345eef8 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts @@ -81,10 +81,7 @@ export function trainedModelsApiProvider(httpService: HttpService) { * @param params - Optional query params */ getTrainedModelStats(modelId?: string | string[], params?: InferenceStatsQueryParams) { - let model = modelId ?? '_all'; - if (Array.isArray(modelId)) { - model = modelId.join(','); - } + const model = (Array.isArray(modelId) ? modelId.join(',') : modelId) || '_all'; return httpService.http({ path: `${apiBasePath}/trained_models/${model}/_stats`, diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index 8bafabf35e1d5f1..97a6fd3eb7b2769 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -181,6 +181,7 @@ export const ModelsList: FC = () => { useEffect( function updateOnTimerRefresh() { + if (!refresh) return; fetchModelsData(); }, [refresh] diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 8b8d361611a2da6..c982cdd5604d188 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n-react'; import moment from 'moment-timezone'; -import { +import type { TypedLensByValueInput, PersistedIndexPatternLayer, PieVisualizationState, diff --git a/x-pack/plugins/reporting/jest.integration.config.js b/x-pack/plugins/reporting/jest.integration.config.js new file mode 100644 index 000000000000000..7f43fa6b4464ae0 --- /dev/null +++ b/x-pack/plugins/reporting/jest.integration.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/x-pack/plugins/reporting'], +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 386e00fc28d8baa..c336b588f12b8b6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -400,7 +400,7 @@ export const BULK_EDIT_FLYOUT_FORM_ADD_TAGS_HELP_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addTagsComboboxHelpText', { defaultMessage: - 'Add one or more custom identifying tags for selected rules. Press enter after each tag to begin a new one.', + 'Add one or more tags for selected rules from the dropdown. You can also enter custom identifying tags and press Enter to begin a new one.', } ); @@ -408,7 +408,7 @@ export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_HELP_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteTagsComboboxHelpText', { defaultMessage: - 'Delete one or more custom identifying tags for selected rules. Press enter after each tag to begin a new one.', + 'Delete one or more tags for selected rules from the dropdown. You can also enter custom identifying tags and press Enter to begin a new one.', } ); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx index fc00953724428d4..bc6eae3fcd5733a 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx @@ -155,6 +155,17 @@ describe('', () => { }); }); + test('term search with a date is parsed', async () => { + await setSearchText('2022.02.10'); + expect(useLoadSnapshots).lastCalledWith({ + ...DEFAULT_SNAPSHOT_LIST_PARAMS, + searchField: 'snapshot', + searchValue: '2022.02.10', + searchMatch: 'must', + searchOperator: 'eq', + }); + }); + test('excluding term search is converted to partial excluding snapshot search', async () => { await setSearchText('-test_snapshot'); expect(useLoadSnapshots).lastCalledWith({ diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts b/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts index b75a3e01fb617bf..20276ae58b8e41b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts +++ b/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts @@ -6,6 +6,7 @@ */ import { Direction, Query } from '@elastic/eui'; +import { SchemaType } from '@elastic/eui/src/components/search_bar/search_box'; export type SortField = | 'snapshot' @@ -49,12 +50,15 @@ const resetSearchOptions = (listParams: SnapshotListParams): SnapshotListParams }); // to init the query for repository and policyName search passed via url -export const getQueryFromListParams = (listParams: SnapshotListParams): Query => { +export const getQueryFromListParams = ( + listParams: SnapshotListParams, + schema: SchemaType +): Query => { const { searchField, searchValue } = listParams; if (!searchField || !searchValue) { return Query.MATCH_ALL; } - return Query.parse(`${searchField}=${searchValue}`); + return Query.parse(`${searchField}=${searchValue}`, { schema }); }; export const getListParams = (listParams: SnapshotListParams, query: Query): SnapshotListParams => { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx index 6f873eacceb51b2..99a160d54d23ebb 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx @@ -126,7 +126,7 @@ export const SnapshotSearchBar: React.FunctionComponent = ({ ); - const [query, setQuery] = useState(getQueryFromListParams(listParams)); + const [query, setQuery] = useState(getQueryFromListParams(listParams, searchSchema)); const [error, setError] = useState(null); const onSearchBarChange = (args: EuiSearchBarOnChangeArgs) => { diff --git a/x-pack/plugins/task_manager/jest.integration.config.js b/x-pack/plugins/task_manager/jest.integration.config.js new file mode 100644 index 000000000000000..e46b3f1bdf13655 --- /dev/null +++ b/x-pack/plugins/task_manager/jest.integration.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/x-pack/plugins/task_manager'], +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a5020c112265153..125c9ff09650713 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -631,17 +631,14 @@ "xpack.lens.pie.addLayer": "ビジュアライゼーションレイヤーを追加", "xpack.lens.pie.arrayValues": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。", "xpack.lens.pie.donutLabel": "ドーナッツ", - "xpack.lens.pie.expressionHelpLabel": "円表示", "xpack.lens.pie.groupLabel": "比率", "xpack.lens.pie.groupsizeLabel": "サイズ単位", "xpack.lens.pie.pielabel": "円", - "xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType}グラフは負の値では表示できません。", "xpack.lens.pie.sliceGroupLabel": "スライス", "xpack.lens.pie.suggestionLabel": "{chartName}として", "xpack.lens.pie.treemapGroupLabel": "グループ分けの条件", "xpack.lens.pie.treemaplabel": "ツリーマップ", "xpack.lens.pie.treemapSuggestionLabel": "ツリーマップとして", - "xpack.lens.pie.visualizationName": "円", "xpack.lens.pieChart.categoriesInLegendLabel": "ラベルを非表示", "xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ", "xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示", @@ -2983,8 +2980,6 @@ "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "値でフィルター", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "値を除外", - "expressionPartitionVis.negativeValuesFound": "円/ドーナツグラフは負の値では表示できません。", - "expressionPartitionVis.noResultsFoundTitle": "結果が見つかりませんでした", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", "fieldFormats.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト{numeralFormatLink}です", "fieldFormats.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ab50cb20956a8f5..69e9f293f845d5b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -643,17 +643,14 @@ "xpack.lens.pie.addLayer": "添加可视化图层", "xpack.lens.pie.arrayValues": "{label} 包含数组值。您的可视化可能无法正常渲染。", "xpack.lens.pie.donutLabel": "圆环图", - "xpack.lens.pie.expressionHelpLabel": "饼图呈现器", "xpack.lens.pie.groupLabel": "比例", "xpack.lens.pie.groupsizeLabel": "大小调整依据", "xpack.lens.pie.pielabel": "饼图", - "xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType} 图表无法使用负值进行呈现。", "xpack.lens.pie.sliceGroupLabel": "切片依据", "xpack.lens.pie.suggestionLabel": "为 {chartName}", "xpack.lens.pie.treemapGroupLabel": "分组依据", "xpack.lens.pie.treemaplabel": "树状图", "xpack.lens.pie.treemapSuggestionLabel": "为树状图", - "xpack.lens.pie.visualizationName": "饼图", "xpack.lens.pieChart.categoriesInLegendLabel": "隐藏标签", "xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部", "xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏", @@ -2767,8 +2764,6 @@ "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "筛留值", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "筛除值", - "expressionPartitionVis.negativeValuesFound": "饼图/圆环图无法使用负值进行呈现。", - "expressionPartitionVis.noResultsFoundTitle": "找不到结果", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", "fieldFormats.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", "fieldFormats.advancedSettings.format.bytesFormatTitle": "字节格式", diff --git a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts index 019c41f9292bad5..3d07f3feacdd214 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts @@ -6,22 +6,22 @@ */ import { take } from 'lodash'; import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; -import { ApmApiSupertest } from '../../common/apm_api_supertest'; +import { ApmServices } from '../../common/config'; export async function getServiceNodeIds({ - apmApiSupertest, + apmApiClient, start, end, serviceName = 'opbeans-java', count = 1, }: { - apmApiSupertest: ApmApiSupertest; + apmApiClient: Awaited>; start: string; end: string; serviceName?: string; count?: number; }) { - const { body } = await apmApiSupertest({ + const { body } = await apmApiClient.readUser({ endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, params: { path: { serviceName }, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts index d0f60ded4c44472..99c2585162fdfe1 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts @@ -4,22 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import url from 'url'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/create_call_apm_api'; import { getServiceNodeIds } from './get_service_node_ids'; -import { createApmApiClient } from '../../common/apm_api_supertest'; type ServiceOverviewInstanceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); - const apmApiSupertest = createApmApiClient(supertest); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; @@ -30,16 +27,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { describe('when data is not loaded', () => { it('handles empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: - '/internal/apm/services/opbeans-java/service_overview_instances/details/foo', + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', + params: { + path: { serviceName: 'opbeans-java', serviceNodeName: 'foo' }, query: { start, end, }, - }) - ); + }, + }); expect(response.status).to.be(200); expect(response.body).to.eql({}); @@ -62,16 +60,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { let serviceNodeIds: string[]; before(async () => { - serviceNodeIds = await getServiceNodeIds({ apmApiSupertest, start, end }); - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/service_overview_instances/details/${serviceNodeIds[0]}`, + serviceNodeIds = await getServiceNodeIds({ + apmApiClient, + start, + end, + }); + + response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', + params: { + path: { serviceName: 'opbeans-node', serviceNodeName: serviceNodeIds[0] }, query: { start, end, }, - }) - ); + }, + }); }); it('returns the instance details', () => { @@ -90,15 +95,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { it('handles empty state when instance id not found', async () => { - const response = await supertest.get( - url.format({ - pathname: '/internal/apm/services/opbeans-java/service_overview_instances/details/foo', + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', + params: { + path: { serviceName: 'opbeans-java', serviceNodeName: 'foo' }, query: { start, end, }, - }) - ); + }, + }); expect(response.status).to.be(200); expect(response.body).to.eql({}); }); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts index b344639920615aa..e8c4b73ac297057 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts @@ -6,21 +6,20 @@ */ import expect from '@kbn/expect'; -import url from 'url'; import moment from 'moment'; import { Coordinate } from '../../../../plugins/apm/typings/timeseries'; +import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/create_call_apm_api'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { createApmApiClient } from '../../common/apm_api_supertest'; import { getServiceNodeIds } from './get_service_node_ids'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); - const apmApiSupertest = createApmApiClient(supertest); + const apmApiClient = getService('apmApiClient'); + const serviceName = 'opbeans-java'; const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; @@ -35,23 +34,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { describe('when data is not loaded', () => { it('handles the empty state', async () => { - const response: Response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/service_overview_instances/detailed_statistics`, + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + params: { + path: { serviceName }, query: { - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, start, end, numBuckets: 20, transactionType: 'request', serviceNodeIds: JSON.stringify( - await getServiceNodeIds({ apmApiSupertest, start, end }) + await getServiceNodeIds({ apmApiClient, start, end }) ), environment: 'ENVIRONMENT_ALL', kuery: '', }, - }) - ); + }, + }); expect(response.status).to.be(200); expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); @@ -69,15 +70,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { let serviceNodeIds: string[]; beforeEach(async () => { - serviceNodeIds = await getServiceNodeIds({ apmApiSupertest, start, end }); + serviceNodeIds = await getServiceNodeIds({ + apmApiClient, + start, + end, + }); }); beforeEach(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/service_overview_instances/detailed_statistics`, + response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + params: { + path: { serviceName }, query: { - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, start, end, numBuckets: 20, @@ -86,8 +93,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { environment: 'ENVIRONMENT_ALL', kuery: '', }, - }) - ); + }, + }); }); it('returns a service node item', () => { @@ -123,15 +130,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { let serviceNodeIds: string[]; beforeEach(async () => { - serviceNodeIds = await getServiceNodeIds({ apmApiSupertest, start, end }); + serviceNodeIds = await getServiceNodeIds({ + apmApiClient, + start, + end, + }); }); beforeEach(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/service_overview_instances/detailed_statistics`, + response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + params: { + path: { serviceName }, query: { - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, numBuckets: 20, transactionType: 'request', serviceNodeIds: JSON.stringify(serviceNodeIds), @@ -142,8 +155,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { environment: 'ENVIRONMENT_ALL', kuery: '', }, - }) - ); + }, + }); }); it('returns a service node item for current and previous periods', () => { diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 75d5c58d8e37590..a803b7224d0b49d 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -12,6 +12,10 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); + const getPackagePolicyById = async (id: string) => { + const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); + return body; + }; // use function () {} and not () => {} here // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions @@ -399,5 +403,51 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); }); + + it('should trim whitespace from policy name', async function () { + const nameWithWhitespace = ' package-policy-with-whitespace-prefix-and-suffix '; + const { body } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: nameWithWhitespace, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + enabled: true, + streams: [ + { + data_stream: { + dataset: 'with_required_variables.log', + type: 'logs', + }, + enabled: true, + vars: { + test_var_required: { + value: 'I am required', + }, + }, + }, + ], + type: 'test_input', + }, + ], + package: { + name: 'with_required_variables', + version: '0.1.0', + }, + }) + .expect(200); + + const policyId = body.item.id; + + const { item: policy } = await getPackagePolicyById(policyId); + + expect(policy.name).to.equal(nameWithWhitespace.trim()); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts index 7d62ea3bf7ec34d..d1fa97b715b7659 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -12,6 +13,11 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const dockerServers = getService('dockerServers'); + const getPackagePolicyById = async (id: string) => { + const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); + return body; + }; + const server = dockerServers.get('registry'); // use function () {} and not () => {} here // because `this` has to point to the Mocha context @@ -138,6 +144,30 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('should trim whitespace from name on update', async function () { + await supertest + .put(`/api/fleet/package_policies/${packagePolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: ' filetest-1 ', + description: '', + namespace: 'updated_namespace', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }); + + const { item: policy } = await getPackagePolicyById(packagePolicyId); + + expect(policy.name).to.equal('filetest-1'); + }); + it('should work with valid values on hosted policies', async function () { await supertest .put(`/api/fleet/package_policies/${packagePolicyId}`) diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts index 26efa4248850bfb..382449e5e2586d9 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'timePicker']); - const find = getService('find'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const dashboardPanelActions = getService('dashboardPanelActions'); @@ -49,8 +48,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.lens.saveAndReturn(); await PageObjects.dashboard.waitForRenderComplete(); - const pieExists = await find.existsByCssSelector('.lnsPieExpression__container'); - expect(pieExists).to.be(true); + const partitionVisExists = await testSubjects.exists('partitionVisChart'); + expect(partitionVisExists).to.be(true); }); it('editing and saving a lens by value panel retains number of panels', async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts index f6692a2edb3bf28..1d2d3f6862e4393 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -103,8 +103,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.lens.saveAndReturn(); await PageObjects.dashboard.waitForRenderComplete(); - const pieExists = await find.existsByCssSelector('.lnsPieExpression__container'); - expect(pieExists).to.be(true); + const partitionVisExists = await testSubjects.exists('partitionVisChart'); + expect(partitionVisExists).to.be(true); }); it('disables save to library button without visualize save permissions', async () => { diff --git a/x-pack/test/functional/apps/ml/model_management/model_list.ts b/x-pack/test/functional/apps/ml/model_management/model_list.ts index aef8f1d95302d26..08fb3b7124aec33 100644 --- a/x-pack/test/functional/apps/ml/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/model_management/model_list.ts @@ -14,8 +14,6 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await ml.trainedModels.createTestTrainedModels('classification', 15, true); await ml.trainedModels.createTestTrainedModels('regression', 15); - await ml.securityUI.loginAsMlPowerUser(); - await ml.navigation.navigateToTrainedModels(); }); after(async () => { @@ -46,6 +44,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await ml.securityUI.loginAsMlPowerUser(); await ml.navigation.navigateToTrainedModels(); + await ml.commonUI.waitForRefreshButtonEnabled(); }); after(async () => { @@ -173,6 +172,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await ml.securityUI.loginAsMlViewer(); await ml.navigation.navigateToTrainedModels(); + await ml.commonUI.waitForRefreshButtonEnabled(); }); after(async () => { diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index d6b75f53578a805..a97c25b2fcbbf4f 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -338,5 +338,9 @@ export function MachineLearningCommonUIProvider({ async waitForDatePickerIndicatorLoaded() { await testSubjects.waitForEnabled('superDatePickerApplyTimeButton'); }, + + async waitForRefreshButtonEnabled() { + await testSubjects.waitForEnabled('~mlRefreshPageButton'); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index f7fd5efefda3392..f0cb2da9efddec6 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -124,7 +124,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const alerting = MachineLearningAlertingProvider(context, commonUI); const swimLane = SwimLaneProvider(context); const trainedModels = TrainedModelsProvider(context, api, commonUI); - const trainedModelsTable = TrainedModelsTableProvider(context); + const trainedModelsTable = TrainedModelsTableProvider(context, commonUI); const mlNodesPanel = MlNodesPanelProvider(context); return { diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index 1f35d7d1f6d3948..3eed354aca4c16d 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -10,7 +10,8 @@ import { ProvidedType } from '@kbn/test'; import { upperFirst } from 'lodash'; import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlCommonUI } from './common_ui'; export interface TrainedModelRowData { id: string; @@ -20,7 +21,10 @@ export interface TrainedModelRowData { export type MlTrainedModelsTable = ProvidedType; -export function TrainedModelsTableProvider({ getService }: FtrProviderContext) { +export function TrainedModelsTableProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -218,6 +222,7 @@ export function TrainedModelsTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('mlTrainedModelRowDetails', { timeout: 1000 }); } }); + await mlCommonUI.waitForRefreshButtonEnabled(); } public async assertTabContent( diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts index 868e649950ba5db..7f9da264664145e 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts @@ -266,7 +266,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await ml.testExecution.logTestStep('select swim lane tile'); const cells = await ml.swimLane.getCells(overallSwimLaneTestSubj); const sampleCell1 = cells[11]; - const sampleCell2 = cells[12]; + const sampleCell2 = cells[cells.length - 1]; await ml.swimLane.selectCells(overallSwimLaneTestSubj, { x1: sampleCell1.x + cellSize, y1: sampleCell1.y + cellSize, @@ -281,6 +281,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { // clickFitToData only works with displayed legend await maps.openLegend(); await maps.clickFitToData(); + await ml.anomalyExplorer.scrollChartsContainerIntoView(); await maps.closeLegend(); await mlScreenshots.takeScreenshot( diff --git a/yarn.lock b/yarn.lock index 296142627216fbb..be06f21acbc1b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13579,10 +13579,10 @@ elastic-apm-http-client@^10.4.0: semver "^6.3.0" stream-chopper "^3.0.1" -elastic-apm-node@^3.28.0: - version "3.28.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.28.0.tgz#74bb0278a711549a45bd8c8b561c23e02e5865e3" - integrity sha512-6P6IAiozEIUDCZGMQ/Lul1c7a10p5uSIIYISxXd7ms+470fvTOL2pKDfE8ygeXyCvvzwjbvuTQC4y4PWKhj8rg== +elastic-apm-node@^3.29.0: + version "3.29.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.29.0.tgz#3e828405adb9e91ed66bb30780268cc30703f46a" + integrity sha512-tPZKoeIJus8mCYXbIcr+jtsU56EQmmUJ+FvcCopp1zB9mCBLrsqdnJ1oXApLmwMAdWn3IpClO1DZi4gmuRNrEA== dependencies: "@elastic/ecs-pino-format" "^1.2.0" after-all-results "^2.0.0"