diff --git a/config/kibana.yml b/config/kibana.yml index 495d2030083d..aac314c09cb3 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -44,7 +44,7 @@ # the username and password that the Kibana server uses to perform maintenance on the Kibana # index at startup. Your Kibana users still need to authenticate with Elasticsearch, which # is proxied through the Kibana server. -#elasticsearch.username: "user" +#elasticsearch.username: "kibana" #elasticsearch.password: "pass" # Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. diff --git a/docs/ml/creating-jobs.asciidoc b/docs/ml/creating-jobs.asciidoc index 181de3ea97b6..d88df4d8e14e 100644 --- a/docs/ml/creating-jobs.asciidoc +++ b/docs/ml/creating-jobs.asciidoc @@ -46,13 +46,12 @@ These wizards create {ml} jobs, dashboards, searches, and visualizations that are customized to help you analyze your {auditbeat} and {filebeat} data. If you are not certain which type of job to create, you can use the -*Data Visualizer* to learn more about your data and to identify possible fields -for {ml} analysis. +*Data Visualizer* to learn more about your data. If your index pattern contains +a time field, it can identify possible fields for {ml} analysis. [NOTE] =============================== -* If your index pattern does not contain a time field, you cannot use the *Data Visualizer*. -* If your data is located outside of {es}, you cannot use {kib} to create +If your data is located outside of {es}, you cannot use {kib} to create your jobs and you cannot use {dfeeds} to retrieve your data in real time. Machine learning analysis is still possible, however, by using APIs to create and manage jobs and post data to them. For more information, see diff --git a/docs/ml/index.asciidoc b/docs/ml/index.asciidoc index 95d3f55da21d..a7571be6d70f 100644 --- a/docs/ml/index.asciidoc +++ b/docs/ml/index.asciidoc @@ -16,8 +16,9 @@ The {ml-features} run in and scale with {es}, and include an intuitive UI on the {kib} *Machine Learning* page for creating anomaly detection jobs and understanding results. -If you have a basic license, you can use the *Data Visualizer* to learn more -about the data that you've stored in {es} and to identify possible fields for +If you have a basic license, you can use the *Data Visualizer* to learn more +about your data. In particular, if your data is stored in {es} and contains a +time field, you can use the *Data Visualizer* to identify possible fields for {ml} analysis: [role="screenshot"] diff --git a/docs/security/authorization/index.asciidoc b/docs/security/authorization/index.asciidoc index 788afedaeb5f..6fc0f3f1367d 100644 --- a/docs/security/authorization/index.asciidoc +++ b/docs/security/authorization/index.asciidoc @@ -20,7 +20,7 @@ To assign {kib} privileges to the role, click **Add space privilege** in the Kib [role="screenshot"] image::security/images/add-space-privileges.png[Add space privileges] -Open the **Spaces** dropdown menu to specify whether to grant the role access to all spaces *** Global (all spaces)** or one or more individual spaces. If you select *** Global (all spaces)**, you can’t select individual spaces until you clear your selection. +Open the **Spaces** selection control to specify whether to grant the role access to all spaces *** Global (all spaces)** or one or more individual spaces. If you select *** Global (all spaces)**, you can’t select individual spaces until you clear your selection. Use the **Privilege** menu to grant access to features. The default is **Custom**, which you can use to grant access to individual features. Otherwise, you can grant read and write access to all current and future features by selecting **All**, or grant read access to all current and future features by selecting **Read**. @@ -39,7 +39,7 @@ image::security/images/create-space-privilege.png[Create space privilege] ==== Assigning different privileges to different spaces -Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added space privileges, click **Add space privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** control. +Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added space privileges, click **Add space privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control. Additionally, if you’ve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, space privileges are also a union. If you’ve already granted the user the **All** privilege at *** Global (all spaces)**, you’re not able to restrict the role to only the **Read** privilege at an individual space. diff --git a/package.json b/package.json index 4680edea5f34..5a629c46f6e1 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@babel/register": "7.4.4", "@elastic/charts": "^4.2.6", "@elastic/datemath": "5.0.2", - "@elastic/eui": "10.4.1", + "@elastic/eui": "11.0.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -145,17 +145,17 @@ "core-js": "2.6.9", "css-loader": "1.0.0", "custom-event-polyfill": "^0.3.0", - "d3": "3.5.6", - "d3-cloud": "1.2.1", + "d3": "3.5.17", + "d3-cloud": "1.2.5", "del": "^4.0.0", "dragula": "3.7.2", "elasticsearch": "^15.5.0", "elasticsearch-browser": "^15.5.0", - "encode-uri-query": "1.0.0", + "encode-uri-query": "1.0.1", "execa": "^1.0.0", "expiry-js": "0.1.7", "file-loader": "2.0.0", - "font-awesome": "4.4.0", + "font-awesome": "4.7.0", "getos": "^3.1.0", "glob": "^7.1.2", "glob-all": "^3.1.0", @@ -165,37 +165,37 @@ "handlebars": "4.1.2", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", - "hjson": "3.1.0", + "hjson": "3.1.2", "hoek": "^5.0.4", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.1", "inert": "^5.1.0", "joi": "^13.5.2", "jquery": "^3.4.1", - "js-yaml": "3.4.1", + "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", - "json-stringify-pretty-compact": "1.0.4", + "json-stringify-pretty-compact": "1.2.0", "json-stringify-safe": "5.0.1", "leaflet": "1.0.3", - "leaflet-draw": "0.4.10", - "leaflet-responsive-popup": "0.2.0", + "leaflet-draw": "0.4.14", + "leaflet-responsive-popup": "0.6.4", "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", "less": "^2.7.3", "less-loader": "4.1.0", "lodash": "npm:@elastic/lodash@3.10.1-kibana1", "lodash.clonedeep": "^4.5.0", - "lru-cache": "4.1.1", + "lru-cache": "4.1.5", "markdown-it": "^8.4.1", "mini-css-extract-plugin": "0.4.4", "minimatch": "^3.0.4", "mkdirp": "0.5.1", "moment": "^2.20.1", "moment-timezone": "^0.5.14", - "mustache": "2.3.0", + "mustache": "2.3.2", "ngreact": "0.5.1", "no-ui-slider": "1.2.0", - "node-fetch": "1.3.2", + "node-fetch": "1.7.3", "opn": "^5.4.0", "oppsy": "^2.0.0", "pegjs": "0.9.0", @@ -258,6 +258,7 @@ }, "devDependencies": { "@babel/parser": "7.4.5", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/types": "7.4.4", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/github-checks-reporter": "0.0.15", @@ -268,7 +269,7 @@ "@kbn/expect": "1.0.0", "@kbn/plugin-generator": "1.0.0", "@kbn/test": "1.0.0", - "@microsoft/api-documenter": "7.0.49", + "@microsoft/api-documenter": "7.2.1", "@microsoft/api-extractor": "7.1.1", "@octokit/rest": "^15.10.0", "@types/angular": "1.6.50", @@ -287,7 +288,7 @@ "@types/enzyme": "^3.1.12", "@types/eslint": "^4.16.6", "@types/execa": "^0.9.0", - "@types/fetch-mock": "7.2.1", + "@types/fetch-mock": "7.3.0", "@types/getopts": "^2.0.1", "@types/glob": "^7.1.1", "@types/globby": "^8.0.0", @@ -302,7 +303,7 @@ "@types/jquery": "^3.3.6", "@types/js-yaml": "^3.11.1", "@types/json5": "^0.0.30", - "@types/listr": "^0.13.0", + "@types/listr": "^0.14.0", "@types/lodash": "^3.10.1", "@types/lru-cache": "^5.1.0", "@types/minimatch": "^2.0.29", @@ -338,6 +339,7 @@ "archiver": "^3.0.0", "babel-eslint": "10.0.1", "babel-jest": "^24.1.0", + "babel-plugin-dynamic-import-node": "^2.2.0", "backport": "4.5.5", "chai": "3.5.0", "chance": "1.0.18", @@ -366,7 +368,7 @@ "eslint-plugin-react-hooks": "1.6.0", "exit-hook": "^2.1.0", "faker": "1.1.0", - "fetch-mock": "7.3.0", + "fetch-mock": "7.3.3", "geckodriver": "1.13.0", "getopts": "^2.2.4", "grunt": "1.0.4", @@ -378,7 +380,7 @@ "gulp-babel": "8.0.0", "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", - "image-diff": "1.6.0", + "image-diff": "1.6.3", "intl-messageformat-parser": "^1.4.0", "is-path-inside": "^2.0.0", "istanbul-instrumenter-loader": "3.0.1", @@ -386,7 +388,7 @@ "jest-cli": "^24.1.0", "jest-dom": "^3.1.3", "jest-raw-loader": "^1.0.1", - "jimp": "0.6.0", + "jimp": "0.6.4", "json5": "^1.0.1", "karma": "3.1.4", "karma-chrome-launcher": "2.2.0", diff --git a/packages/eslint-config-kibana/.eslintrc.js b/packages/eslint-config-kibana/.eslintrc.js index 6d39eb98ddb4..c3e8f5601e47 100644 --- a/packages/eslint-config-kibana/.eslintrc.js +++ b/packages/eslint-config-kibana/.eslintrc.js @@ -7,11 +7,11 @@ module.exports = { plugins: ['@kbn/eslint-plugin-eslint'], parserOptions: { - ecmaVersion: 6 + ecmaVersion: 2018 }, env: { - es6: true + es6: true, }, rules: { diff --git a/packages/eslint-config-kibana/javascript.js b/packages/eslint-config-kibana/javascript.js index a5433fa6b5d9..f1ed905779fb 100644 --- a/packages/eslint-config-kibana/javascript.js +++ b/packages/eslint-config-kibana/javascript.js @@ -44,8 +44,7 @@ module.exports = { parserOptions: { sourceType: 'module', - ecmaVersion: 6, - ecmaFeatures: { experimentalObjectRestSpread: true }, + ecmaVersion: 2018, }, rules: { diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 9bce8503d32f..26a5db48d6c2 100644 --- a/packages/eslint-config-kibana/typescript.js +++ b/packages/eslint-config-kibana/typescript.js @@ -42,9 +42,8 @@ module.exports = { parserOptions: { sourceType: 'module', - ecmaVersion: 6, + ecmaVersion: 2018, ecmaFeatures: { - experimentalObjectRestSpread: true, jsx: true }, // NOTE: That is to avoid a known performance issue related with the `ts.Program` used by diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/disallow_license_headers.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/disallow_license_headers.js index 61fc1c112b8b..0bf71d717bad 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/__tests__/disallow_license_headers.js +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/disallow_license_headers.js @@ -24,7 +24,7 @@ const dedent = require('dedent'); const ruleTester = new RuleTester({ parser: 'babel-eslint', parserOptions: { - ecmaVersion: 2015, + ecmaVersion: 2018, }, }); diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js index 33d178519d06..11f01a1777ab 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js @@ -35,7 +35,7 @@ const ruleTester = new RuleTester({ parser: 'babel-eslint', parserOptions: { sourceType: 'module', - ecmaVersion: 2015, + ecmaVersion: 2018, }, }); diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/require_license_header.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/require_license_header.js index 491ceb2290be..984c090ceb06 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/__tests__/require_license_header.js +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/require_license_header.js @@ -24,7 +24,7 @@ const dedent = require('dedent'); const ruleTester = new RuleTester({ parser: 'babel-eslint', parserOptions: { - ecmaVersion: 2015, + ecmaVersion: 2018, }, }); diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 2c64b560c81e..58b1e10507db 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -19,7 +19,7 @@ "globby": "^8.0.1", "gulp-babel": "^8.0.0", "gulp-rename": "1.4.0", - "gulp-zip": "4.1.0", + "gulp-zip": "4.2.0", "inquirer": "^1.2.2", "minimatch": "^3.0.4", "node-sass": "^4.9.4", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index ed596d341b02..32c9c9173ed6 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -33,7 +33,7 @@ "@types/read-pkg": "^3.0.0", "@types/strip-ansi": "^3.0.0", "@types/strong-log-transformer": "^1.0.0", - "@types/tempy": "^0.1.0", + "@types/tempy": "^0.2.0", "@types/wrap-ansi": "^2.0.14", "@types/write-pkg": "^3.1.0", "babel-loader": "8.0.5", @@ -60,7 +60,7 @@ "string-replace-loader": "^2.1.1", "strip-ansi": "^4.0.0", "strong-log-transformer": "^2.1.0", - "tempy": "^0.2.1", + "tempy": "^0.3.0", "typescript": "^3.3.3333", "unlazy-loader": "^0.1.3", "webpack": "^4.23.1", diff --git a/renovate.json5 b/renovate.json5 index 35c225b664fa..bb5faa01bee1 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -106,6 +106,12 @@ groupName: 'grunt related packages', groupSlug: 'grunt', }, + { + packagePatterns: ['\\bangular\\b'], + groupName: 'angular related packages', + groupSlug: 'angular', + recreateClosed: false, + }, { packagePatterns: ['\\bd3\\b'], groupName: 'd3 related packages', diff --git a/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json index 64fcbf1f7488..12307c46b95f 100644 --- a/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json +++ b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json @@ -3,7 +3,7 @@ "strict": true, "skipLibCheck": true, "lib": [ - "esnext" + "es2018" ] }, "files": [ diff --git a/src/dev/jest/babel_transform.js b/src/dev/jest/babel_transform.js index 0796cf859d4e..c31843313731 100644 --- a/src/dev/jest/babel_transform.js +++ b/src/dev/jest/babel_transform.js @@ -23,4 +23,9 @@ module.exports = babelJest.createTransformer({ presets: [ require.resolve('@kbn/babel-preset/node_preset') ], + plugins: [ + // enables jest to parse and execute dynamic import() calls + '@babel/plugin-syntax-dynamic-import', + 'dynamic-import-node' + ] }); diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 2c71b354e520..e1a0c4a87392 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -88,7 +88,8 @@ export default { '^.+\\.html?$': 'jest-raw-loader', }, transformIgnorePatterns: [ - '[/\\\\]node_modules[/\\\\].+\\.js$', + // ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import() + '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js' ], snapshotSerializers: [ diff --git a/src/dev/license_checker/config.js b/src/dev/license_checker/config.js index 6c72c918279d..e8adfc90c438 100644 --- a/src/dev/license_checker/config.js +++ b/src/dev/license_checker/config.js @@ -60,7 +60,7 @@ export const LICENSE_WHITELIST = [ 'MIT*', 'MIT/X11', 'new BSD, and MIT', - 'OFL-1.1 AND MIT', + '(OFL-1.1 AND MIT)', 'Public Domain', 'Unlicense', 'WTFPL OR ISC', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap index 747b92bb5a5d..107bda0aea08 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap @@ -33,24 +33,13 @@ exports[`DashboardPanel matches snapshot 1`] = ` > + /> diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/__snapshots__/no_results.test.js.snap b/src/legacy/core_plugins/kibana/public/discover/directives/__snapshots__/no_results.test.js.snap index ebc27316d467..9f400e54899a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/directives/__snapshots__/no_results.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/discover/directives/__snapshots__/no_results.test.js.snap @@ -20,25 +20,13 @@ Array [ > + /> @@ -223,25 +211,13 @@ Array [ > + /> @@ -274,25 +250,13 @@ Array [ > + /> @@ -393,25 +357,13 @@ Array [ > + /> diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap index 05d968c3d4e3..82d2698b42e9 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap @@ -522,24 +522,20 @@ exports[`bulkCreate should display success message when bulkCreate is successful title="complete" type="check" > - - + diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap index 8461271d9c14..ca04ac8fcfaa 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -135,16 +135,11 @@ exports[`Table should render normally 1`] = ` exports[`Table should render the boolean template (false) 1`] = ``; exports[`Table should render the boolean template (true) 1`] = ` - `; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js index b94898c7ea3b..9de71f808d00 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js @@ -32,7 +32,6 @@ import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/r import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; import { UICapabilitiesProvider } from 'ui/capabilities/react'; -import { EuiBadge } from '@elastic/eui'; import { getListBreadcrumbs } from './breadcrumbs'; import React from 'react'; @@ -124,36 +123,39 @@ uiModules.get('apps/management') }); const renderList = () => { - $scope.indexPatternList = $route.current.locals.indexPatterns.map(pattern => { - const id = pattern.id; - const tags = indexPatternListProvider.getIndexPatternTags(pattern, $scope.defaultIndex === id); + $scope.indexPatternList = + $route.current.locals.indexPatterns + .map(pattern => { + const id = pattern.id; + const title = pattern.get('title'); + const isDefault = $scope.defaultIndex === id; + const tags = indexPatternListProvider.getIndexPatternTags( + pattern, + isDefault + ); - return { - id: id, - title: - - {pattern.get('title')}{$scope.defaultIndex === id && (Default)} - , - url: kbnUrl.eval('#/management/kibana/index_patterns/{{id}}', { id: id }), - active: $scope.editingId === id, - default: $scope.defaultIndex === id, - tag: tags && tags.length ? tags[0] : null, - }; - }).sort((a, b) => { - if(a.default) { - return -1; - } - if(b.default) { - return 1; - } - if(a.title < b.title) { - return -1; - } - if(a.title > b.title) { - return 1; - } - return 0; - }) || []; + return { + id, + title, + url: kbnUrl.eval('#/management/kibana/index_patterns/{{id}}', { id: id }), + active: $scope.editingId === id, + default: isDefault, + tag: tags && tags.length ? tags[0] : null, + //the prepending of 0 at the default pattern takes care of prioritization + //so the sorting will but the default index on top + //or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${title}`, + }; + }) + .sort((a, b) => { + if (a.sort < b.sort) { + return -1; + } else if (a.sort > b.sort) { + return 1; + } else { + return 0; + } + }) || []; updateIndexPatternList($scope.indexPatternList, kbnUrl, indexPatternCreationOptions); }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx index 79ff3d8c624d..2e4134bf9b6b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx @@ -18,6 +18,7 @@ */ import { + EuiBadge, EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, @@ -38,13 +39,14 @@ const columns = [ { field: 'title', name: 'Pattern', - render: (name: string, { id }: { id: string }) => ( - + render: (name: string, index: { id: string; default: boolean }) => ( + {name} + {index.default && Default} ), dataType: 'string', - sortable: true, + sortable: ({ sort }: { sort: string }) => sort, }, ]; @@ -56,7 +58,7 @@ const pagination = { const sorting = { sort: { field: 'title', - direction: 'desc', + direction: 'asc', }, }; @@ -90,9 +92,9 @@ export class IndexPatternTable extends React.Component { this.setState({ showFlyout: false })} /> )} - + - +

{ + />
+ />
@@ -288,7 +273,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` >
+ />
@@ -571,7 +541,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` >
+ />
@@ -793,7 +748,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` >
+ />
@@ -1730,7 +1638,7 @@ exports[`NewVisModal should render as expected 1`] = ` >
+ />
@@ -2010,7 +1903,7 @@ exports[`NewVisModal should render as expected 1`] = ` >
+ />
@@ -2229,7 +2107,7 @@ exports[`NewVisModal should render as expected 1`] = ` > renders as expected 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero euiAccordion__iconWrapper" > - - - - - + />
renders as expected with actions 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero euiAccordion__iconWrapper" > - - - - - + />
renders as expected with actions 1`] = ` > + />
@@ -219,26 +184,13 @@ exports[` renders as expected with initial collapsed 1`] = class="euiFlexItem euiFlexItem--flexGrowZero euiAccordion__iconWrapper" > - - - - - + />
(myPos + halfSize)) { - startPos = myPos + halfSize; - return this.textContent; - } else { - d3.select(this.parentNode).remove(); - } - }); - }; - } - - draw() { - const self = this; - const config = this.axisConfig; - - return function (selection) { - selection.each(function () { - selection.selectAll('text') - .attr('style', function () { - const currentStyle = d3.select(this).attr('style'); - return `${currentStyle} font-size: ${config.get('labels.fontSize')};`; - }); - if (!config.get('labels.show')) selection.selectAll('text').attr('style', 'display: none;'); - - selection.call(self.truncateLabels()); - selection.call(self.rotateAxisLabels()); - selection.call(self.filterAxisLabels()); - }); - }; - } +export class AxisLabels { + constructor(axisConfig, scale) { + this.axisConfig = axisConfig; + this.axisScale = scale; } - return AxisLabels; + render(selection) { + selection.call(this.draw()); + } + + rotateAxisLabels() { + const config = this.axisConfig; + return function (selection) { + const text = selection.selectAll('.tick text'); + + if (config.get('labels.rotate')) { + text + .style('text-anchor', function () { + const currentValue = $(this).css('text-anchor'); + const rotateDeg = config.get('labels.rotate'); + if (!rotateDeg) return currentValue; + else { + const position = config.get('position'); + switch (position) { + case 'top': return 'end'; + case 'bottom': return 'end'; + default: + if (rotateDeg === 90 || rotateDeg === -90) return 'middle'; + return currentValue; + } + } + }) + .attr('dy', function () { + return config.isHorizontal() ? '0.3em' : '0'; + }) + .attr('transform', function rotate(d, j) { + const position = config.get('position'); + const rotateDeg = position === 'top' ? config.get('labels.rotate') : -config.get('labels.rotate'); + + if ($(this).css('text-anchor') === 'middle') { + const coord = text[0][j].getBBox(); + const transX = ((coord.x) + (coord.width / 2)); + const transY = ((coord.y) + (coord.height / 2)); + return `rotate(${rotateDeg}, ${transX}, ${transY})`; + } else { + const transX = this.attributes.x.nodeValue; + const transY = this.attributes.y.nodeValue; + return `rotate(${rotateDeg}, ${transX}, ${transY})`; + } + }); + } + }; + } + + truncateLabels() { + const config = this.axisConfig; + return function (selection) { + if (!config.get('labels.truncate')) return; + + selection.selectAll('.tick text') + .text(function () { + return truncateLabel(this, config.get('labels.truncate')); + }); + }; + } + + filterAxisLabels() { + const self = this; + const config = this.axisConfig; + let startPos = 0; + const padding = 1.1; + + return function (selection) { + if (!config.get('labels.filter')) return; + + const el = $(config.get('rootEl')).find(config.get('elSelector')); + const maxSize = config.isHorizontal() ? el.width() : el.height(); + const scaleRange = self.axisScale.scale.range(); + const scaleWidth = Math.abs(scaleRange[scaleRange.length - 1] - scaleRange[0]); + const scaleStartPad = .5 * (maxSize - scaleWidth); + + selection.selectAll('.tick text') + .text(function (d) { + const par = d3.select(this.parentNode).node(); + const myPos = scaleStartPad + self.axisScale.scale(d); + const mySize = (config.isHorizontal() ? par.getBBox().width : par.getBBox().height) * padding; + const halfSize = mySize / 2; + + if ((startPos + halfSize) < myPos && maxSize > (myPos + halfSize)) { + startPos = myPos + halfSize; + return this.textContent; + } else { + d3.select(this.parentNode).remove(); + } + }); + }; + } + + draw() { + const self = this; + const config = this.axisConfig; + + return function (selection) { + selection.each(function () { + selection.selectAll('text') + .attr('style', function () { + const currentStyle = d3.select(this).attr('style'); + return `${currentStyle} font-size: ${config.get('labels.fontSize')};`; + }); + if (!config.get('labels.show')) selection.selectAll('text').attr('style', 'display: none;'); + + selection.call(self.truncateLabels()); + selection.call(self.rotateAxisLabels()); + selection.call(self.filterAxisLabels()); + }); + }; + } } diff --git a/src/legacy/ui/public/vislib/visualizations/_chart.js b/src/legacy/ui/public/vislib/visualizations/_chart.js index b01f6d2177ec..a65dfbae7d3c 100644 --- a/src/legacy/ui/public/vislib/visualizations/_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/_chart.js @@ -21,7 +21,7 @@ import d3 from 'd3'; import _ from 'lodash'; import { dataLabel } from '../lib/_data_label'; import { VislibLibDispatchProvider } from '../lib/dispatch'; -import { TooltipProvider } from '../../vis/components/tooltip'; +import { Tooltip } from '../../vis/components/tooltip'; import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities'; import { HierarchicalTooltipFormatterProvider } from '../../agg_response/hierarchical/_hierarchical_tooltip_formatter'; import { PointSeriesTooltipFormatter } from '../../agg_response/point_series/_tooltip_formatter'; @@ -29,7 +29,6 @@ import { PointSeriesTooltipFormatter } from '../../agg_response/point_series/_to export function VislibVisualizationsChartProvider(Private) { const Dispatch = Private(VislibLibDispatchProvider); - const Tooltip = Private(TooltipProvider); /** * The Base Class for all visualizations. * diff --git a/src/legacy/ui/public/vislib/visualizations/gauge_chart.js b/src/legacy/ui/public/vislib/visualizations/gauge_chart.js index da47e4235435..279cc4f60016 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauge_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/gauge_chart.js @@ -19,11 +19,10 @@ import d3 from 'd3'; import { VislibVisualizationsChartProvider } from './_chart'; -import { GaugeTypesProvider } from './gauges/gauge_types'; +import { gaugeTypes } from './gauges/gauge_types'; export function GaugeChartProvider(Private) { const Chart = Private(VislibVisualizationsChartProvider); - const gaugeTypes = Private(GaugeTypesProvider); class GaugeChart extends Chart { constructor(handler, chartEl, chartData) { diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/gauge_types.js b/src/legacy/ui/public/vislib/visualizations/gauges/gauge_types.js index 3c9b0e77f66d..450a51401146 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauges/gauge_types.js +++ b/src/legacy/ui/public/vislib/visualizations/gauges/gauge_types.js @@ -17,11 +17,8 @@ * under the License. */ -import { MeterGaugeProvider } from './meter'; +import { MeterGauge } from './meter'; -export function GaugeTypesProvider(Private) { - - return { - meter: Private(MeterGaugeProvider) - }; -} +export const gaugeTypes = { + meter: MeterGauge +}; diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js index f407dbc96367..834d579b2d93 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js +++ b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js @@ -21,326 +21,321 @@ import d3 from 'd3'; import _ from 'lodash'; import { getHeatmapColors } from '../../components/color/heatmap_color'; -export function MeterGaugeProvider() { +const defaultConfig = { + showTooltip: true, + percentageMode: true, + maxAngle: 2 * Math.PI * 1.3, + minAngle: 2 * Math.PI * 0.7, + innerSpace: 5, + extents: [0, 10000], + scale: { + show: true, + color: '#666', + width: 2, + ticks: 10, + tickLength: 8, + }, + labels: { + show: true, + color: '#666' + }, + style: { + bgWidth: 0.5, + width: 0.9 + } +}; - const defaultConfig = { - showTooltip: true, - percentageMode: true, - maxAngle: 2 * Math.PI * 1.3, - minAngle: 2 * Math.PI * 0.7, - innerSpace: 5, - extents: [0, 10000], - scale: { - show: true, - color: '#666', - width: 2, - ticks: 10, - tickLength: 8, - }, - labels: { - show: true, - color: '#666' - }, - style: { - bgWidth: 0.5, - width: 0.9 - } - }; +export class MeterGauge { + constructor(gaugeChart) { + this.gaugeChart = gaugeChart; + this.gaugeConfig = gaugeChart.gaugeConfig; + this.gaugeConfig = _.defaultsDeep(this.gaugeConfig, defaultConfig); - class MeterGauge { - constructor(gaugeChart) { - this.gaugeChart = gaugeChart; - this.gaugeConfig = gaugeChart.gaugeConfig; - this.gaugeConfig = _.defaultsDeep(this.gaugeConfig, defaultConfig); + this.gaugeChart.handler.visConfig.set('legend', { + labels: this.getLabels(), + colors: this.getColors() + }); - this.gaugeChart.handler.visConfig.set('legend', { - labels: this.getLabels(), - colors: this.getColors() - }); - - const colors = this.gaugeChart.handler.visConfig.get('legend.colors', null); - if (colors) { - this.gaugeChart.handler.vis.uiState.setSilent('vis.defaultColors', null); - this.gaugeChart.handler.vis.uiState.setSilent('vis.defaultColors', colors); - } - - this.colorFunc = this.gaugeChart.handler.data.getColorFunc(); + const colors = this.gaugeChart.handler.visConfig.get('legend.colors', null); + if (colors) { + this.gaugeChart.handler.vis.uiState.setSilent('vis.defaultColors', null); + this.gaugeChart.handler.vis.uiState.setSilent('vis.defaultColors', colors); } - getLabels() { - const isPercentageMode = this.gaugeConfig.percentageMode; - const colorsRange = this.gaugeConfig.colorsRange; - const max = _.last(colorsRange).to; - const labels = []; - colorsRange.forEach(range => { - const from = isPercentageMode ? Math.round(100 * range.from / max) : range.from; - const to = isPercentageMode ? Math.round(100 * range.to / max) : range.to; - labels.push(`${from} - ${to}`); - }); - - return labels; - } - - getColors() { - const invertColors = this.gaugeConfig.invertColors; - const colorSchema = this.gaugeConfig.colorSchema; - const colorsRange = this.gaugeConfig.colorsRange; - const labels = this.getLabels(); - const colors = {}; - for (let i = 0; i < labels.length; i += 1) { - const divider = Math.max(colorsRange.length - 1, 1); - const val = invertColors ? 1 - i / divider : i / divider; - colors[labels[i]] = getHeatmapColors(val, colorSchema); - } - return colors; - } - - getBucket(val) { - let bucket = _.findIndex(this.gaugeConfig.colorsRange, range => { - return range.from <= val && range.to > val; - }); - - if (bucket === -1) { - if (val < this.gaugeConfig.colorsRange[0].from) bucket = 0; - else bucket = this.gaugeConfig.colorsRange.length - 1; - } - - return bucket; - } - - getLabel(val) { - const bucket = this.getBucket(val); - const labels = this.gaugeChart.handler.visConfig.get('legend.labels'); - return labels[bucket]; - } - - getColorBucket(val) { - const bucket = this.getBucket(val); - const labels = this.gaugeChart.handler.visConfig.get('legend.labels'); - return this.colorFunc(labels[bucket]); - } - - drawScale(svg, radius, angle) { - const scaleWidth = this.gaugeConfig.scale.width; - const tickLength = this.gaugeConfig.scale.tickLength; - const scaleTicks = this.gaugeConfig.scale.ticks; - - const scale = svg.append('g'); - - this.gaugeConfig.colorsRange.forEach(range => { - const color = this.getColorBucket(range.from); - - const scaleArc = d3.svg.arc() - .startAngle(angle(range.from)) - .endAngle(angle(range.to)) - .innerRadius(radius) - .outerRadius(radius + scaleWidth); - - scale - .append('path') - .attr('d', scaleArc) - .style('stroke', color) - .style('fill', color); - }); - - - const extents = angle.domain(); - for (let i = 0; i <= scaleTicks; i++) { - const val = i * (extents[1] - extents[0]) / scaleTicks; - const tickAngle = angle(val) - Math.PI / 2; - const x0 = Math.cos(tickAngle) * radius; - const x1 = Math.cos(tickAngle) * (radius - tickLength); - const y0 = Math.sin(tickAngle) * radius; - const y1 = Math.sin(tickAngle) * (radius - tickLength); - const color = this.getColorBucket(val); - scale.append('line') - .attr('x1', x0).attr('x2', x1) - .attr('y1', y0).attr('y2', y1) - .style('stroke-width', scaleWidth) - .style('stroke', color); - } - - return scale; - } - - drawGauge(svg, data, width, height) { - const self = this; - const marginFactor = 0.95; - const tooltip = this.gaugeChart.tooltip; - const isTooltip = this.gaugeChart.handler.visConfig.get('addTooltip'); - const isDisplayWarning = this.gaugeChart.handler.visConfig.get('isDisplayWarning', false); - const maxAngle = this.gaugeConfig.maxAngle; - const minAngle = this.gaugeConfig.minAngle; - const angleFactor = this.gaugeConfig.gaugeType === 'Arc' ? 0.75 : 1; - const maxRadius = (Math.min(width, height / angleFactor) / 2) * marginFactor; - - const extendRange = this.gaugeConfig.extendRange; - const maxY = _.max(data.values, 'y').y; - const min = this.gaugeConfig.colorsRange[0].from; - const max = _.last(this.gaugeConfig.colorsRange).to; - const angle = d3.scale.linear() - .range([minAngle, maxAngle]) - .domain([min, extendRange && max < maxY ? maxY : max]); - const radius = d3.scale.linear() - .range([0, maxRadius]) - .domain([this.gaugeConfig.innerSpace + 1, 0]); - - const totalWidth = Math.abs(radius(0) - radius(1)); - const bgPadding = totalWidth * (1 - this.gaugeConfig.style.bgWidth) / 2; - const gaugePadding = totalWidth * (1 - this.gaugeConfig.style.width) / 2; - - /** - * Function to calculate the free space in the center of the gauge. This takes into account - * whether ticks are enabled or not. - * - * This is calculated using the inner diameter (radius(1) * 2) of the gauge. - * If ticks/scale are enabled we need to still subtract the tick length * 2 to make space for a tick - * on every side. If ticks/scale are disabled, the radius(1) function actually leaves space for the scale, - * so we add that free space (which is expressed via the paddings, we just use the larger of those) to the diameter. - */ - const getInnerFreeSpace = () => (radius(1) * 2) - - (this.gaugeConfig.scale.show - ? this.gaugeConfig.scale.tickLength * 2 - : -Math.max(bgPadding, gaugePadding) * 2 - ); - - const arc = d3.svg.arc() - .startAngle(minAngle) - .endAngle(function (d) { - return Math.max(0, Math.min(maxAngle, angle(Math.max(min, d.y)))); - }) - .innerRadius(function (d, i, j) { - return Math.max(0, radius(j + 1) + gaugePadding); - }) - .outerRadius(function (d, i, j) { - return Math.max(0, radius(j) - gaugePadding); - }); - - const bgArc = d3.svg.arc() - .startAngle(minAngle) - .endAngle(maxAngle) - .innerRadius(function (d, i, j) { - return Math.max(0, radius(j + 1) + bgPadding); - }) - .outerRadius(function (d, i, j) { - return Math.max(0, radius(j) - bgPadding); - }); - - const gaugeHolders = svg - .selectAll('path') - .data([data]) - .enter() - .append('g') - .attr('data-label', (d) => this.getLabel(d.values[0].y)); - - - const gauges = gaugeHolders - .selectAll('g') - .data(d => d.values) - .enter(); - - - gauges - .append('path') - .attr('d', bgArc) - .style('fill', this.gaugeConfig.style.bgFill); - - const series = gauges - .append('path') - .attr('d', arc) - .style('fill', function (d) { - return self.getColorBucket(Math.max(min, d.y)); - }); - - const smallContainer = svg.node().getBBox().height < 70; - let hiddenLabels = smallContainer; - - // If the value label is hidden we later want to hide also all other labels - // since they don't make sense as long as the actual value is hidden. - let valueLabelHidden = false; - - gauges - .append('text') - .attr('class', 'chart-label') - .attr('y', -5) - .text(d => { - if (this.gaugeConfig.percentageMode) { - const percentage = Math.round(100 * (d.y - min) / (max - min)); - return `${percentage}%`; - } - return data.yAxisFormatter(d.y); - }) - .attr('style', 'dominant-baseline: central;') - .style('text-anchor', 'middle') - .style('font-size', '2em') - .style('display', function () { - const textLength = this.getBBox().width; - // The text is too long if it's larger than the inner free space minus a couple of random pixels for padding. - const textTooLong = textLength >= getInnerFreeSpace() - 6; - if (textTooLong) { - hiddenLabels = true; - valueLabelHidden = true; - } - return textTooLong ? 'none' : 'initial'; - }); - - if (this.gaugeConfig.labels.show) { - svg - .append('text') - .attr('class', 'chart-label') - .text(data.label) - .attr('y', -30) - .attr('style', 'dominant-baseline: central; text-anchor: middle;') - .style('display', function () { - const textLength = this.getBBox().width; - const textTooLong = textLength > maxRadius; - if (textTooLong) { - hiddenLabels = true; - } - return smallContainer || textTooLong ? 'none' : 'initial'; - }); - - svg - .append('text') - .attr('class', 'chart-label') - .text(this.gaugeConfig.style.subText) - .attr('y', 20) - .attr('style', 'dominant-baseline: central; text-anchor: middle;') - .style('display', function () { - const textLength = this.getBBox().width; - const textTooLong = textLength > maxRadius; - if (textTooLong) { - hiddenLabels = true; - } - return valueLabelHidden || smallContainer || textTooLong ? 'none' : 'initial'; - }); - } - - if (this.gaugeConfig.scale.show) { - this.drawScale(svg, radius(1), angle); - } - - if (isTooltip) { - series.each(function () { - const gauge = d3.select(this); - gauge.call(tooltip.render()); - }); - } - - if (hiddenLabels && isDisplayWarning) { - this.gaugeChart.handler.alerts.show('Some labels were hidden due to size constraints'); - } - - //center the visualization - const transformX = width / 2; - const transformY = height / 2 > maxRadius ? height / 2 : maxRadius; - - svg.attr('transform', `translate(${transformX}, ${transformY})`); - - return series; - } + this.colorFunc = this.gaugeChart.handler.data.getColorFunc(); } - return MeterGauge; + getLabels() { + const isPercentageMode = this.gaugeConfig.percentageMode; + const colorsRange = this.gaugeConfig.colorsRange; + const max = _.last(colorsRange).to; + const labels = []; + colorsRange.forEach(range => { + const from = isPercentageMode ? Math.round(100 * range.from / max) : range.from; + const to = isPercentageMode ? Math.round(100 * range.to / max) : range.to; + labels.push(`${from} - ${to}`); + }); + + return labels; + } + + getColors() { + const invertColors = this.gaugeConfig.invertColors; + const colorSchema = this.gaugeConfig.colorSchema; + const colorsRange = this.gaugeConfig.colorsRange; + const labels = this.getLabels(); + const colors = {}; + for (let i = 0; i < labels.length; i += 1) { + const divider = Math.max(colorsRange.length - 1, 1); + const val = invertColors ? 1 - i / divider : i / divider; + colors[labels[i]] = getHeatmapColors(val, colorSchema); + } + return colors; + } + + getBucket(val) { + let bucket = _.findIndex(this.gaugeConfig.colorsRange, range => { + return range.from <= val && range.to > val; + }); + + if (bucket === -1) { + if (val < this.gaugeConfig.colorsRange[0].from) bucket = 0; + else bucket = this.gaugeConfig.colorsRange.length - 1; + } + + return bucket; + } + + getLabel(val) { + const bucket = this.getBucket(val); + const labels = this.gaugeChart.handler.visConfig.get('legend.labels'); + return labels[bucket]; + } + + getColorBucket(val) { + const bucket = this.getBucket(val); + const labels = this.gaugeChart.handler.visConfig.get('legend.labels'); + return this.colorFunc(labels[bucket]); + } + + drawScale(svg, radius, angle) { + const scaleWidth = this.gaugeConfig.scale.width; + const tickLength = this.gaugeConfig.scale.tickLength; + const scaleTicks = this.gaugeConfig.scale.ticks; + + const scale = svg.append('g'); + + this.gaugeConfig.colorsRange.forEach(range => { + const color = this.getColorBucket(range.from); + + const scaleArc = d3.svg.arc() + .startAngle(angle(range.from)) + .endAngle(angle(range.to)) + .innerRadius(radius) + .outerRadius(radius + scaleWidth); + + scale + .append('path') + .attr('d', scaleArc) + .style('stroke', color) + .style('fill', color); + }); + + + const extents = angle.domain(); + for (let i = 0; i <= scaleTicks; i++) { + const val = i * (extents[1] - extents[0]) / scaleTicks; + const tickAngle = angle(val) - Math.PI / 2; + const x0 = Math.cos(tickAngle) * radius; + const x1 = Math.cos(tickAngle) * (radius - tickLength); + const y0 = Math.sin(tickAngle) * radius; + const y1 = Math.sin(tickAngle) * (radius - tickLength); + const color = this.getColorBucket(val); + scale.append('line') + .attr('x1', x0).attr('x2', x1) + .attr('y1', y0).attr('y2', y1) + .style('stroke-width', scaleWidth) + .style('stroke', color); + } + + return scale; + } + + drawGauge(svg, data, width, height) { + const self = this; + const marginFactor = 0.95; + const tooltip = this.gaugeChart.tooltip; + const isTooltip = this.gaugeChart.handler.visConfig.get('addTooltip'); + const isDisplayWarning = this.gaugeChart.handler.visConfig.get('isDisplayWarning', false); + const maxAngle = this.gaugeConfig.maxAngle; + const minAngle = this.gaugeConfig.minAngle; + const angleFactor = this.gaugeConfig.gaugeType === 'Arc' ? 0.75 : 1; + const maxRadius = (Math.min(width, height / angleFactor) / 2) * marginFactor; + + const extendRange = this.gaugeConfig.extendRange; + const maxY = _.max(data.values, 'y').y; + const min = this.gaugeConfig.colorsRange[0].from; + const max = _.last(this.gaugeConfig.colorsRange).to; + const angle = d3.scale.linear() + .range([minAngle, maxAngle]) + .domain([min, extendRange && max < maxY ? maxY : max]); + const radius = d3.scale.linear() + .range([0, maxRadius]) + .domain([this.gaugeConfig.innerSpace + 1, 0]); + + const totalWidth = Math.abs(radius(0) - radius(1)); + const bgPadding = totalWidth * (1 - this.gaugeConfig.style.bgWidth) / 2; + const gaugePadding = totalWidth * (1 - this.gaugeConfig.style.width) / 2; + + /** + * Function to calculate the free space in the center of the gauge. This takes into account + * whether ticks are enabled or not. + * + * This is calculated using the inner diameter (radius(1) * 2) of the gauge. + * If ticks/scale are enabled we need to still subtract the tick length * 2 to make space for a tick + * on every side. If ticks/scale are disabled, the radius(1) function actually leaves space for the scale, + * so we add that free space (which is expressed via the paddings, we just use the larger of those) to the diameter. + */ + const getInnerFreeSpace = () => (radius(1) * 2) - + (this.gaugeConfig.scale.show + ? this.gaugeConfig.scale.tickLength * 2 + : -Math.max(bgPadding, gaugePadding) * 2 + ); + + const arc = d3.svg.arc() + .startAngle(minAngle) + .endAngle(function (d) { + return Math.max(0, Math.min(maxAngle, angle(Math.max(min, d.y)))); + }) + .innerRadius(function (d, i, j) { + return Math.max(0, radius(j + 1) + gaugePadding); + }) + .outerRadius(function (d, i, j) { + return Math.max(0, radius(j) - gaugePadding); + }); + + const bgArc = d3.svg.arc() + .startAngle(minAngle) + .endAngle(maxAngle) + .innerRadius(function (d, i, j) { + return Math.max(0, radius(j + 1) + bgPadding); + }) + .outerRadius(function (d, i, j) { + return Math.max(0, radius(j) - bgPadding); + }); + + const gaugeHolders = svg + .selectAll('path') + .data([data]) + .enter() + .append('g') + .attr('data-label', (d) => this.getLabel(d.values[0].y)); + + + const gauges = gaugeHolders + .selectAll('g') + .data(d => d.values) + .enter(); + + + gauges + .append('path') + .attr('d', bgArc) + .style('fill', this.gaugeConfig.style.bgFill); + + const series = gauges + .append('path') + .attr('d', arc) + .style('fill', function (d) { + return self.getColorBucket(Math.max(min, d.y)); + }); + + const smallContainer = svg.node().getBBox().height < 70; + let hiddenLabels = smallContainer; + + // If the value label is hidden we later want to hide also all other labels + // since they don't make sense as long as the actual value is hidden. + let valueLabelHidden = false; + + gauges + .append('text') + .attr('class', 'chart-label') + .attr('y', -5) + .text(d => { + if (this.gaugeConfig.percentageMode) { + const percentage = Math.round(100 * (d.y - min) / (max - min)); + return `${percentage}%`; + } + return data.yAxisFormatter(d.y); + }) + .attr('style', 'dominant-baseline: central;') + .style('text-anchor', 'middle') + .style('font-size', '2em') + .style('display', function () { + const textLength = this.getBBox().width; + // The text is too long if it's larger than the inner free space minus a couple of random pixels for padding. + const textTooLong = textLength >= getInnerFreeSpace() - 6; + if (textTooLong) { + hiddenLabels = true; + valueLabelHidden = true; + } + return textTooLong ? 'none' : 'initial'; + }); + + if (this.gaugeConfig.labels.show) { + svg + .append('text') + .attr('class', 'chart-label') + .text(data.label) + .attr('y', -30) + .attr('style', 'dominant-baseline: central; text-anchor: middle;') + .style('display', function () { + const textLength = this.getBBox().width; + const textTooLong = textLength > maxRadius; + if (textTooLong) { + hiddenLabels = true; + } + return smallContainer || textTooLong ? 'none' : 'initial'; + }); + + svg + .append('text') + .attr('class', 'chart-label') + .text(this.gaugeConfig.style.subText) + .attr('y', 20) + .attr('style', 'dominant-baseline: central; text-anchor: middle;') + .style('display', function () { + const textLength = this.getBBox().width; + const textTooLong = textLength > maxRadius; + if (textTooLong) { + hiddenLabels = true; + } + return valueLabelHidden || smallContainer || textTooLong ? 'none' : 'initial'; + }); + } + + if (this.gaugeConfig.scale.show) { + this.drawScale(svg, radius(1), angle); + } + + if (isTooltip) { + series.each(function () { + const gauge = d3.select(this); + gauge.call(tooltip.render()); + }); + } + + if (hiddenLabels && isDisplayWarning) { + this.gaugeChart.handler.alerts.show('Some labels were hidden due to size constraints'); + } + + //center the visualization + const transformX = width / 2; + const transformY = height / 2 > maxRadius ? height / 2 : maxRadius; + + svg.attr('transform', `translate(${transformX}, ${transformY})`); + + return series; + } } diff --git a/src/legacy/ui/public/vislib/visualizations/point_series.js b/src/legacy/ui/public/vislib/visualizations/point_series.js index 8883be5ae977..c2da64d94d80 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series.js @@ -20,17 +20,15 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import { TooltipProvider } from '../../vis/components/tooltip'; +import { Tooltip } from '../../vis/components/tooltip'; import { VislibVisualizationsChartProvider } from './_chart'; -import { VislibVisualizationsTimeMarkerProvider } from './time_marker'; -import { VislibVisualizationsSeriesTypesProvider } from './point_series/series_types'; +import { TimeMarker } from './time_marker'; +import { seriesTypes } from './point_series/series_types'; export function VislibVisualizationsPointSeriesProvider(Private) { const Chart = Private(VislibVisualizationsChartProvider); - const Tooltip = Private(TooltipProvider); - const TimeMarker = Private(VislibVisualizationsTimeMarkerProvider); - const seriTypes = Private(VislibVisualizationsSeriesTypesProvider); + const seriTypes = seriesTypes; const touchdownTmpl = _.template(require('../partials/touchdown.tmpl.html')); /** * Line Chart Visualization diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js b/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js index a1cd8a403b0e..dd02a542f35c 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js @@ -19,70 +19,65 @@ import _ from 'lodash'; -export function VislibVisualizationsPointSeriesProvider() { - - class PointSeries { - constructor(handler, seriesEl, seriesData, seriesConfig) { - this.handler = handler; - this.baseChart = handler.pointSeries; - this.chartEl = seriesEl; - this.chartData = seriesData; - this.seriesConfig = seriesConfig; - } - - getGroupedCount() { - const stacks = []; - return this.baseChart.chartConfig.series.reduce((sum, series) => { - const valueAxis = series.valueAxis || this.baseChart.handler.valueAxes[0].id; - const isStacked = series.mode === 'stacked'; - const isHistogram = series.type === 'histogram'; - if (!isHistogram) return sum; - if (isStacked && stacks.includes(valueAxis)) return sum; - if (isStacked) stacks.push(valueAxis); - return sum + 1; - }, 0); - } - - getGroupedNum(data) { - let i = 0; - const stacks = []; - for (const seri of this.baseChart.chartConfig.series) { - const valueAxis = seri.valueAxis || this.baseChart.handler.valueAxes[0].id; - const isStacked = seri.mode === 'stacked'; - if (!isStacked) { - if (seri.data === data) return i; - i++; - } else { - if (!(valueAxis in stacks)) stacks[valueAxis] = i++; - if (seri.data === data) return stacks[valueAxis]; - } - } - return 0; - } - - getValueAxis() { - return _.find(this.handler.valueAxes, axis => { - return axis.axisConfig.get('id') === this.seriesConfig.valueAxis; - }) || this.handler.valueAxes[0]; - } - - getCategoryAxis() { - return _.find(this.handler.categoryAxes, axis => { - return axis.axisConfig.get('id') === this.seriesConfig.categoryAxis; - }) || this.handler.categoryAxes[0]; - } - - addCircleEvents(element) { - const events = this.events; - if (this.handler.visConfig.get('enableHover')) { - const hover = events.addHoverEvent(); - const mouseout = events.addMouseoutEvent(); - element.call(hover).call(mouseout); - } - const click = events.addClickEvent(); - return element.call(click); - } +export class PointSeries { + constructor(handler, seriesEl, seriesData, seriesConfig) { + this.handler = handler; + this.baseChart = handler.pointSeries; + this.chartEl = seriesEl; + this.chartData = seriesData; + this.seriesConfig = seriesConfig; } - return PointSeries; + getGroupedCount() { + const stacks = []; + return this.baseChart.chartConfig.series.reduce((sum, series) => { + const valueAxis = series.valueAxis || this.baseChart.handler.valueAxes[0].id; + const isStacked = series.mode === 'stacked'; + const isHistogram = series.type === 'histogram'; + if (!isHistogram) return sum; + if (isStacked && stacks.includes(valueAxis)) return sum; + if (isStacked) stacks.push(valueAxis); + return sum + 1; + }, 0); + } + + getGroupedNum(data) { + let i = 0; + const stacks = []; + for (const seri of this.baseChart.chartConfig.series) { + const valueAxis = seri.valueAxis || this.baseChart.handler.valueAxes[0].id; + const isStacked = seri.mode === 'stacked'; + if (!isStacked) { + if (seri.data === data) return i; + i++; + } else { + if (!(valueAxis in stacks)) stacks[valueAxis] = i++; + if (seri.data === data) return stacks[valueAxis]; + } + } + return 0; + } + + getValueAxis() { + return _.find(this.handler.valueAxes, axis => { + return axis.axisConfig.get('id') === this.seriesConfig.valueAxis; + }) || this.handler.valueAxes[0]; + } + + getCategoryAxis() { + return _.find(this.handler.categoryAxes, axis => { + return axis.axisConfig.get('id') === this.seriesConfig.categoryAxis; + }) || this.handler.categoryAxes[0]; + } + + addCircleEvents(element) { + const events = this.events; + if (this.handler.visConfig.get('enableHover')) { + const hover = events.addHoverEvent(); + const mouseout = events.addMouseoutEvent(); + element.call(hover).call(mouseout); + } + const click = events.addClickEvent(); + return element.call(click); + } } diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/area_chart.js b/src/legacy/ui/public/vislib/visualizations/point_series/area_chart.js index 24e9a507197c..9bcc35ef70c3 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/area_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/area_chart.js @@ -20,250 +20,244 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import { VislibVisualizationsPointSeriesProvider } from './_point_series'; +import { PointSeries } from './_point_series'; -export function VislibVisualizationsAreaChartProvider(Private) { - const PointSeries = Private(VislibVisualizationsPointSeriesProvider); +const defaults = { + mode: 'normal', + showCircles: true, + radiusRatio: 9, + showLines: true, + interpolate: 'linear', + color: undefined, + fillColor: undefined, +}; +/** + * Area chart visualization + * + * @class AreaChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific + * chart + */ +export class AreaChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); - const defaults = { - mode: 'normal', - showCircles: true, - radiusRatio: 9, - showLines: true, - interpolate: 'linear', - color: undefined, - fillColor: undefined, - }; - /** - * Area chart visualization - * - * @class AreaChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific - * chart - */ - class AreaChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs) { - super(handler, chartEl, chartData, seriesConfigArgs); + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + this.isOverlapping = (this.seriesConfig.mode !== 'stacked'); + if (this.isOverlapping) { - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - this.isOverlapping = (this.seriesConfig.mode !== 'stacked'); - if (this.isOverlapping) { + // Default opacity should return to 0.6 on mouseout + const defaultOpacity = 0.6; + this.seriesConfig.defaultOpacity = defaultOpacity; + handler.highlight = function (element) { + const label = this.getAttribute('data-label'); + if (!label) return; - // Default opacity should return to 0.6 on mouseout - const defaultOpacity = 0.6; - this.seriesConfig.defaultOpacity = defaultOpacity; - handler.highlight = function (element) { - const label = this.getAttribute('data-label'); - if (!label) return; - - const highlightOpacity = 0.8; - const highlightElements = $('[data-label]', element.parentNode).filter( - function (els, el) { - return `${$(el).data('label')}` === label; - }); - $('[data-label]', element.parentNode).not(highlightElements).css('opacity', defaultOpacity / 2); // half of the default opacity - highlightElements.css('opacity', highlightOpacity); - }; - handler.unHighlight = function (element) { - $('[data-label]', element).css('opacity', defaultOpacity); - - //The legend should keep max opacity - $('[data-label]', $(element).siblings()).css('opacity', 1); - }; - } - - } - - addPath(svg, data) { - const ordered = this.handler.data.get('ordered'); - const isTimeSeries = (ordered && ordered.date); - const isOverlapping = this.isOverlapping; - const color = this.handler.data.getColorFunc(); - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const interpolate = this.seriesConfig.interpolate; - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - - // Data layers - const layer = svg.append('g') - .attr('class', function (d, i) { - return 'series series-' + i; - }); - - // Append path - const path = layer.append('path') - .attr('data-label', data.label) - .style('fill', () => color(data.label)) - .style('stroke', () => color(data.label)) - .classed('visAreaChart__overlapArea', function () { - return isOverlapping; - }) - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - function x(d) { - if (isTimeSeries) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function y1(d) { - const y0 = d.y0 || 0; - const y = d.y || 0; - return yScale(y0 + y); - } - - function y0(d) { - const y0 = d.y0 || 0; - return yScale(y0); - } - - function getArea() { - if (isHorizontal) { - return d3.svg.area() - .x(x) - .y0(y0) - .y1(y1); - } else { - return d3.svg.area() - .y(x) - .x0(y0) - .x1(y1); - } - } - - // update - path - .attr('d', function () { - const area = getArea() - .defined(function (d) { - return !_.isNull(d.y); - }) - .interpolate(interpolate); - return area(data.values); - }) - .style('stroke-width', '1px'); - - return path; - } - - /** - * Adds SVG circles to area chart - * - * @method addCircles - * @param svg {HTMLElement} SVG to which circles are appended - * @param data {Array} Chart data array - * @returns {D3.UpdateSelection} SVG with circles added - */ - addCircles(svg, data) { - const color = this.handler.data.getColorFunc(); - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const ordered = this.handler.data.get('ordered'); - const circleRadius = 12; - const circleStrokeWidth = 0; - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isOverlapping = this.isOverlapping; - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - - const layer = svg.append('g') - .attr('class', 'points area') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - // append the circles - const circles = layer.selectAll('circles') - .data(function appendData() { - return data.values.filter(function isZeroOrNull(d) { - return d.y !== 0 && !_.isNull(d.y); + const highlightOpacity = 0.8; + const highlightElements = $('[data-label]', element.parentNode).filter( + function (els, el) { + return `${$(el).data('label')}` === label; }); - }); - - // exit - circles.exit().remove(); - - // enter - circles - .enter() - .append('circle') - .attr('data-label', data.label) - .attr('stroke', () => { - return color(data.label); - }) - .attr('fill', 'transparent') - .attr('stroke-width', circleStrokeWidth); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - const y = d.y || 0; - if (isOverlapping) { - return yScale(y); - } - return yScale(d.y0 + y); - } - - // update - circles - .attr('cx', isHorizontal ? cx : cy) - .attr('cy', isHorizontal ? cy : cx) - .attr('r', circleRadius); - - // Add tooltip - if (isTooltip) { - circles.call(tooltip.render()); - } - - return circles; - } - - addPathEvents(path) { - const events = this.events; - if (this.handler.visConfig.get('enableHover')) { - const hover = events.addHoverEvent(); - const mouseout = events.addMouseoutEvent(); - path.call(hover).call(mouseout); - } - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the area chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const path = self.addPath(svg, self.chartData); - self.addPathEvents(path); - const circles = self.addCircles(svg, self.chartData); - self.addCircleEvents(circles); - - self.events.emit('rendered', { - chart: self.chartData - }); - - return svg; - }); + $('[data-label]', element.parentNode).not(highlightElements).css('opacity', defaultOpacity / 2); // half of the default opacity + highlightElements.css('opacity', highlightOpacity); }; + handler.unHighlight = function (element) { + $('[data-label]', element).css('opacity', defaultOpacity); + + //The legend should keep max opacity + $('[data-label]', $(element).siblings()).css('opacity', 1); + }; + } + + } + + addPath(svg, data) { + const ordered = this.handler.data.get('ordered'); + const isTimeSeries = (ordered && ordered.date); + const isOverlapping = this.isOverlapping; + const color = this.handler.data.getColorFunc(); + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const interpolate = this.seriesConfig.interpolate; + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + // Data layers + const layer = svg.append('g') + .attr('class', function (d, i) { + return 'series series-' + i; + }); + + // Append path + const path = layer.append('path') + .attr('data-label', data.label) + .style('fill', () => color(data.label)) + .style('stroke', () => color(data.label)) + .classed('visAreaChart__overlapArea', function () { + return isOverlapping; + }) + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + function x(d) { + if (isTimeSeries) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function y1(d) { + const y0 = d.y0 || 0; + const y = d.y || 0; + return yScale(y0 + y); + } + + function y0(d) { + const y0 = d.y0 || 0; + return yScale(y0); + } + + function getArea() { + if (isHorizontal) { + return d3.svg.area() + .x(x) + .y0(y0) + .y1(y1); + } else { + return d3.svg.area() + .y(x) + .x0(y0) + .x1(y1); + } + } + + // update + path + .attr('d', function () { + const area = getArea() + .defined(function (d) { + return !_.isNull(d.y); + }) + .interpolate(interpolate); + return area(data.values); + }) + .style('stroke-width', '1px'); + + return path; + } + + /** + * Adds SVG circles to area chart + * + * @method addCircles + * @param svg {HTMLElement} SVG to which circles are appended + * @param data {Array} Chart data array + * @returns {D3.UpdateSelection} SVG with circles added + */ + addCircles(svg, data) { + const color = this.handler.data.getColorFunc(); + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const ordered = this.handler.data.get('ordered'); + const circleRadius = 12; + const circleStrokeWidth = 0; + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isOverlapping = this.isOverlapping; + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + const layer = svg.append('g') + .attr('class', 'points area') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + // append the circles + const circles = layer.selectAll('circles') + .data(function appendData() { + return data.values.filter(function isZeroOrNull(d) { + return d.y !== 0 && !_.isNull(d.y); + }); + }); + + // exit + circles.exit().remove(); + + // enter + circles + .enter() + .append('circle') + .attr('data-label', data.label) + .attr('stroke', () => { + return color(data.label); + }) + .attr('fill', 'transparent') + .attr('stroke-width', circleStrokeWidth); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function cy(d) { + const y = d.y || 0; + if (isOverlapping) { + return yScale(y); + } + return yScale(d.y0 + y); + } + + // update + circles + .attr('cx', isHorizontal ? cx : cy) + .attr('cy', isHorizontal ? cy : cx) + .attr('r', circleRadius); + + // Add tooltip + if (isTooltip) { + circles.call(tooltip.render()); + } + + return circles; + } + + addPathEvents(path) { + const events = this.events; + if (this.handler.visConfig.get('enableHover')) { + const hover = events.addHoverEvent(); + const mouseout = events.addMouseoutEvent(); + path.call(hover).call(mouseout); } } - return AreaChart; + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the area chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + const path = self.addPath(svg, self.chartData); + self.addPathEvents(path); + const circles = self.addCircles(svg, self.chartData); + self.addCircleEvents(circles); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + } } diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js b/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js index 7d43d4b47e92..6e0dd2851df8 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js @@ -18,263 +18,257 @@ */ import _ from 'lodash'; -import { VislibVisualizationsPointSeriesProvider } from './_point_series'; +import { PointSeries } from './_point_series'; -export function VislibVisualizationsColumnChartProvider(Private) { - const PointSeries = Private(VislibVisualizationsPointSeriesProvider); +const defaults = { + mode: 'normal', + showTooltip: true, + color: undefined, + fillColor: undefined, +}; - const defaults = { - mode: 'normal', - showTooltip: true, - color: undefined, - fillColor: undefined, - }; - - /** - * Histogram intervals are not always equal widths, e.g, monthly time intervals. - * It is more visually appealing to vary bar width so that gutter width is constant. - */ - function datumWidth(defaultWidth, datum, nextDatum, scale, gutterWidth, groupCount = 1) { - let datumWidth = defaultWidth; - if (nextDatum) { - datumWidth = ((scale(nextDatum.x) - scale(datum.x)) - gutterWidth) / groupCount; - // To handle data-sets with holes, do not let width be larger than default. - if (datumWidth > defaultWidth) { - datumWidth = defaultWidth; - } - } - return datumWidth; - } - - /** - * Vertical Bar Chart Visualization: renders vertical and/or stacked bars - * - * @class ColumnChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ - class ColumnChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs) { - super(handler, chartEl, chartData, seriesConfigArgs); - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - } - - addBars(svg, data) { - const self = this; - const color = this.handler.data.getColorFunc(); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - - const layer = svg.append('g') - .attr('class', 'series histogram') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - const bars = layer.selectAll('rect') - .data(data.values.filter(function (d) { - return !_.isNull(d.y); - })); - - bars - .exit() - .remove(); - - bars - .enter() - .append('rect') - .attr('data-label', data.label) - .attr('fill', () => color(data.label)) - .attr('stroke', () => color(data.label)); - - self.updateBars(bars); - - // Add tooltip - if (isTooltip) { - bars.call(tooltip.render()); - } - - return bars; - } - - /** - * Determines whether bars are grouped or stacked and updates the D3 - * selection - * - * @method updateBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - updateBars(bars) { - if (this.seriesConfig.mode === 'stacked') { - return this.addStackedBars(bars); - } - return this.addGroupedBars(bars); - - } - - /** - * Adds stacked bars to column chart visualization - * - * @method addStackedBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - addStackedBars(bars) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); - const yMin = yScale.domain()[0]; - const gutterSpacingPercentage = 0.15; - const groupCount = this.getGroupedCount(); - const groupNum = this.getGroupedNum(this.chartData); - let barWidth; - let gutterWidth; - - if (isTimeScale) { - const { min, interval } = this.handler.data.get('ordered'); - let intervalWidth = xScale(min + interval) - xScale(min); - intervalWidth = Math.abs(intervalWidth); - - gutterWidth = intervalWidth * gutterSpacingPercentage; - barWidth = (intervalWidth - gutterWidth) / groupCount; - } - - function x(d, i) { - if (isTimeScale) { - return xScale(d.x) + datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount) * groupNum; - } - return xScale(d.x) + xScale.rangeBand() / groupCount * groupNum; - } - - function y(d) { - if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { - return yScale(d.y0); - } - return yScale(d.y0 + d.y); - } - - function widthFunc(d, i) { - if (isTimeScale) { - return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount); - } - return xScale.rangeBand() / groupCount; - } - - function heightFunc(d) { - // for split bars or for one series, - // last series will have d.y0 = 0 - if (d.y0 === 0 && yMin > 0) { - return yScale(yMin) - yScale(d.y); - } - - return Math.abs(yScale(d.y0) - yScale(d.y0 + d.y)); - } - - // update - bars - .attr('x', isHorizontal ? x : y) - .attr('width', isHorizontal ? widthFunc : heightFunc) - .attr('y', isHorizontal ? y : x) - .attr('height', isHorizontal ? heightFunc : widthFunc); - - return bars; - } - - /** - * Adds grouped bars to column chart visualization - * - * @method addGroupedBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - addGroupedBars(bars) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const groupCount = this.getGroupedCount(); - const groupNum = this.getGroupedNum(this.chartData); - const gutterSpacingPercentage = 0.15; - const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const isLogScale = this.getValueAxis().axisConfig.isLogScale(); - let barWidth; - let gutterWidth; - - if (isTimeScale) { - const { min, interval } = this.handler.data.get('ordered'); - let intervalWidth = xScale(min + interval) - xScale(min); - intervalWidth = Math.abs(intervalWidth); - - gutterWidth = intervalWidth * gutterSpacingPercentage; - barWidth = (intervalWidth - gutterWidth) / groupCount; - } - - function x(d, i) { - if (isTimeScale) { - return xScale(d.x) + datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount) * groupNum; - } - return xScale(d.x) + xScale.rangeBand() / groupCount * groupNum; - } - - function y(d) { - if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { - return yScale(0); - } - - return yScale(d.y); - } - - function widthFunc(d, i) { - if (isTimeScale) { - return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount); - } - return xScale.rangeBand() / groupCount; - } - - function heightFunc(d) { - const baseValue = isLogScale ? 1 : 0; - return Math.abs(yScale(baseValue) - yScale(d.y)); - } - - // update - bars - .attr('x', isHorizontal ? x : y) - .attr('width', isHorizontal ? widthFunc : heightFunc) - .attr('y', isHorizontal ? y : x) - .attr('height', isHorizontal ? heightFunc : widthFunc); - - return bars; - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the vertical bar chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const bars = self.addBars(svg, self.chartData); - self.addCircleEvents(bars); - - self.events.emit('rendered', { - chart: self.chartData - }); - - return svg; - }); - }; +/** + * Histogram intervals are not always equal widths, e.g, monthly time intervals. + * It is more visually appealing to vary bar width so that gutter width is constant. + */ +function datumWidth(defaultWidth, datum, nextDatum, scale, gutterWidth, groupCount = 1) { + let datumWidth = defaultWidth; + if (nextDatum) { + datumWidth = ((scale(nextDatum.x) - scale(datum.x)) - gutterWidth) / groupCount; + // To handle data-sets with holes, do not let width be larger than default. + if (datumWidth > defaultWidth) { + datumWidth = defaultWidth; } } - - return ColumnChart; + return datumWidth; +} + +/** + * Vertical Bar Chart Visualization: renders vertical and/or stacked bars + * + * @class ColumnChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ +export class ColumnChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + } + + addBars(svg, data) { + const self = this; + const color = this.handler.data.getColorFunc(); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + + const layer = svg.append('g') + .attr('class', 'series histogram') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + const bars = layer.selectAll('rect') + .data(data.values.filter(function (d) { + return !_.isNull(d.y); + })); + + bars + .exit() + .remove(); + + bars + .enter() + .append('rect') + .attr('data-label', data.label) + .attr('fill', () => color(data.label)) + .attr('stroke', () => color(data.label)); + + self.updateBars(bars); + + // Add tooltip + if (isTooltip) { + bars.call(tooltip.render()); + } + + return bars; + } + + /** + * Determines whether bars are grouped or stacked and updates the D3 + * selection + * + * @method updateBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + updateBars(bars) { + if (this.seriesConfig.mode === 'stacked') { + return this.addStackedBars(bars); + } + return this.addGroupedBars(bars); + + } + + /** + * Adds stacked bars to column chart visualization + * + * @method addStackedBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + addStackedBars(bars) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); + const yMin = yScale.domain()[0]; + const gutterSpacingPercentage = 0.15; + const groupCount = this.getGroupedCount(); + const groupNum = this.getGroupedNum(this.chartData); + let barWidth; + let gutterWidth; + + if (isTimeScale) { + const { min, interval } = this.handler.data.get('ordered'); + let intervalWidth = xScale(min + interval) - xScale(min); + intervalWidth = Math.abs(intervalWidth); + + gutterWidth = intervalWidth * gutterSpacingPercentage; + barWidth = (intervalWidth - gutterWidth) / groupCount; + } + + function x(d, i) { + if (isTimeScale) { + return xScale(d.x) + datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount) * groupNum; + } + return xScale(d.x) + xScale.rangeBand() / groupCount * groupNum; + } + + function y(d) { + if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { + return yScale(d.y0); + } + return yScale(d.y0 + d.y); + } + + function widthFunc(d, i) { + if (isTimeScale) { + return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount); + } + return xScale.rangeBand() / groupCount; + } + + function heightFunc(d) { + // for split bars or for one series, + // last series will have d.y0 = 0 + if (d.y0 === 0 && yMin > 0) { + return yScale(yMin) - yScale(d.y); + } + + return Math.abs(yScale(d.y0) - yScale(d.y0 + d.y)); + } + + // update + bars + .attr('x', isHorizontal ? x : y) + .attr('width', isHorizontal ? widthFunc : heightFunc) + .attr('y', isHorizontal ? y : x) + .attr('height', isHorizontal ? heightFunc : widthFunc); + + return bars; + } + + /** + * Adds grouped bars to column chart visualization + * + * @method addGroupedBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + addGroupedBars(bars) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const groupCount = this.getGroupedCount(); + const groupNum = this.getGroupedNum(this.chartData); + const gutterSpacingPercentage = 0.15; + const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const isLogScale = this.getValueAxis().axisConfig.isLogScale(); + let barWidth; + let gutterWidth; + + if (isTimeScale) { + const { min, interval } = this.handler.data.get('ordered'); + let intervalWidth = xScale(min + interval) - xScale(min); + intervalWidth = Math.abs(intervalWidth); + + gutterWidth = intervalWidth * gutterSpacingPercentage; + barWidth = (intervalWidth - gutterWidth) / groupCount; + } + + function x(d, i) { + if (isTimeScale) { + return xScale(d.x) + datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount) * groupNum; + } + return xScale(d.x) + xScale.rangeBand() / groupCount * groupNum; + } + + function y(d) { + if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { + return yScale(0); + } + + return yScale(d.y); + } + + function widthFunc(d, i) { + if (isTimeScale) { + return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount); + } + return xScale.rangeBand() / groupCount; + } + + function heightFunc(d) { + const baseValue = isLogScale ? 1 : 0; + return Math.abs(yScale(baseValue) - yScale(d.y)); + } + + // update + bars + .attr('x', isHorizontal ? x : y) + .attr('width', isHorizontal ? widthFunc : heightFunc) + .attr('y', isHorizontal ? y : x) + .attr('height', isHorizontal ? heightFunc : widthFunc); + + return bars; + } + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the vertical bar chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + const bars = self.addBars(svg, self.chartData); + self.addCircleEvents(bars); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + } } diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/heatmap_chart.js b/src/legacy/ui/public/vislib/visualizations/point_series/heatmap_chart.js index b765ccb31838..c5dfd18626db 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/heatmap_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/heatmap_chart.js @@ -19,314 +19,308 @@ import _ from 'lodash'; import moment from 'moment'; -import { VislibVisualizationsPointSeriesProvider } from './_point_series'; +import { PointSeries } from './_point_series'; import { getHeatmapColors } from '../../components/color/heatmap_color'; import { isColorDark } from '@elastic/eui'; -export function VislibVisualizationsHeatmapChartProvider(Private) { - const PointSeries = Private(VislibVisualizationsPointSeriesProvider); +const defaults = { + color: undefined, // todo + fillColor: undefined // todo +}; +/** + * Line Chart Visualization + * + * @class HeatmapChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ +export class HeatmapChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - const defaults = { - color: undefined, // todo - fillColor: undefined // todo - }; - /** - * Line Chart Visualization - * - * @class HeatmapChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ - class HeatmapChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs) { - super(handler, chartEl, chartData, seriesConfigArgs); - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + this.handler.visConfig.set('legend', { + labels: this.getHeatmapLabels(this.handler.visConfig), + colors: this.getHeatmapColors(this.handler.visConfig) + }); - this.handler.visConfig.set('legend', { - labels: this.getHeatmapLabels(this.handler.visConfig), - colors: this.getHeatmapColors(this.handler.visConfig) - }); - - const colors = this.handler.visConfig.get('legend.colors', null); - if (colors) { - this.handler.vis.uiState.setSilent('vis.defaultColors', null); - this.handler.vis.uiState.setSilent('vis.defaultColors', colors); - } - } - - getHeatmapLabels(cfg) { - const percentageMode = cfg.get('percentageMode'); - const colorsNumber = cfg.get('colorsNumber'); - const colorsRange = cfg.get('colorsRange'); - const zAxisConfig = this.getValueAxis().axisConfig; - const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); - const zScale = this.getValueAxis().getScale(); - const [min, max] = zScale.domain(); - const labels = []; - const maxColorCnt = 10; - if (cfg.get('setColorRange')) { - colorsRange.forEach(range => { - const from = isFinite(range.from) ? zAxisFormatter(range.from) : range.from; - const to = isFinite(range.to) ? zAxisFormatter(range.to) : range.to; - labels.push(`${from} - ${to}`); - }); - } else { - if (max === min) { - return [ min.toString() ]; - } - for (let i = 0; i < colorsNumber; i++) { - let label; - let val = i / colorsNumber; - let nextVal = (i + 1) / colorsNumber; - if (percentageMode) { - val = Math.ceil(val * 100); - nextVal = Math.ceil(nextVal * 100); - label = `${val}% - ${nextVal}%`; - } else { - val = val * (max - min) + min; - nextVal = nextVal * (max - min) + min; - if (max - min > maxColorCnt) { - const valInt = Math.ceil(val); - if (i === 0) { - val = (valInt === val ? val : valInt - 1); - } - else{ - val = valInt; - } - nextVal = Math.ceil(nextVal); - } - if (isFinite(val)) val = zAxisFormatter(val); - if (isFinite(nextVal)) nextVal = zAxisFormatter(nextVal); - label = `${val} - ${nextVal}`; - } - - labels.push(label); - } - } - - return labels; - } - - getHeatmapColors(cfg) { - const invertColors = cfg.get('invertColors'); - const colorSchema = cfg.get('colorSchema'); - const labels = this.getHeatmapLabels(cfg); - const colors = {}; - for (const i in labels) { - if (labels[i]) { - const val = invertColors ? 1 - i / labels.length : i / labels.length; - colors[labels[i]] = getHeatmapColors(val, colorSchema); - } - } - return colors; - } - - addSquares(svg, data) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.handler.categoryAxes[1].getScale(); - const zScale = this.getValueAxis().getScale(); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const colorsNumber = this.handler.visConfig.get('colorsNumber'); - const setColorRange = this.handler.visConfig.get('setColorRange'); - const colorsRange = this.handler.visConfig.get('colorsRange'); - const color = this.handler.data.getColorFunc(); - const labels = this.handler.visConfig.get('legend.labels'); - const zAxisConfig = this.getValueAxis().axisConfig; - const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); - const showLabels = zAxisConfig.get('labels.show'); - const overwriteLabelColor = zAxisConfig.get('labels.overwriteColor', false); - - const layer = svg.append('g') - .attr('class', 'series'); - - const squares = layer - .selectAll('g.square') - .data(data.values); - - squares - .exit() - .remove(); - - let barWidth; - if (this.getCategoryAxis().axisConfig.isTimeDomain()) { - const { min, interval } = this.handler.data.get('ordered'); - const start = min; - const end = moment(min).add(interval).valueOf(); - - barWidth = xScale(end) - xScale(start); - if (!isHorizontal) barWidth *= -1; - } - - function x(d) { - return xScale(d.x); - } - - function y(d) { - return yScale(d.series); - } - - const [min, max] = zScale.domain(); - function getColorBucket(d) { - let val = 0; - if (setColorRange && colorsRange.length) { - const bucket = _.find(colorsRange, range => { - return range.from <= d.y && range.to > d.y; - }); - return bucket ? colorsRange.indexOf(bucket) : -1; - } else { - if (isNaN(min) || isNaN(max)) { - val = colorsNumber - 1; - } else if (min === max) { - val = 0; - } else { - val = (d.y - min) / (max - min); /* get val from 0 - 1 */ - val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber)); - } - } - if (d.y == null) { - return -1; - } - return !isNaN(val) ? val : -1; - } - - function label(d) { - const colorBucket = getColorBucket(d); - // colorBucket id should always GTE 0 - if (colorBucket < 0) d.hide = true; - return labels[colorBucket]; - } - - function z(d) { - if (label(d) === '') return 'transparent'; - return color(label(d)); - } - - const squareWidth = barWidth || xScale.rangeBand(); - const squareHeight = yScale.rangeBand(); - - squares - .enter() - .append('g') - .attr('class', 'square'); - - squares.append('rect') - .attr('x', x) - .attr('width', squareWidth) - .attr('y', y) - .attr('height', squareHeight) - .attr('data-label', label) - .attr('fill', z) - .attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px') - .style('display', d => { - return d.hide ? 'none' : 'initial'; - }); - - - // todo: verify that longest label is not longer than the barwidth - // or barwidth is not smaller than textheight (and vice versa) - // - if (showLabels) { - const rotate = zAxisConfig.get('labels.rotate'); - const rotateRad = rotate * Math.PI / 180; - const cellPadding = 5; - const maxLength = Math.min( - Math.abs(squareWidth / Math.cos(rotateRad)), - Math.abs(squareHeight / Math.sin(rotateRad)) - ) - cellPadding; - const maxHeight = Math.min( - Math.abs(squareWidth / Math.sin(rotateRad)), - Math.abs(squareHeight / Math.cos(rotateRad)) - ) - cellPadding; - - let labelColor; - if (overwriteLabelColor) { - // If overwriteLabelColor is true, use the manual specified color - labelColor = zAxisConfig.get('labels.color'); - } else { - // Otherwise provide a function that will calculate a light or dark color - labelColor = d => { - const bgColor = z(d); - const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); - return color && isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])) - ? '#FFF' : '#222'; - }; - } - - let hiddenLabels = false; - squares.append('text') - .text(d => zAxisFormatter(d.y)) - .style('display', function (d) { - const textLength = this.getBBox().width; - const textHeight = this.getBBox().height; - const textTooLong = textLength > maxLength; - const textTooWide = textHeight > maxHeight; - if (!d.hide && (textTooLong || textTooWide)) { - hiddenLabels = true; - } - return d.hide || textTooLong || textTooWide ? 'none' : 'initial'; - }) - .style('dominant-baseline', 'central') - .style('text-anchor', 'middle') - .style('fill', labelColor) - .attr('x', function (d) { - const center = x(d) + squareWidth / 2; - return center; - }) - .attr('y', function (d) { - const center = y(d) + squareHeight / 2; - return center; - }) - .attr('transform', function (d) { - const horizontalCenter = x(d) + squareWidth / 2; - const verticalCenter = y(d) + squareHeight / 2; - return `rotate(${rotate},${horizontalCenter},${verticalCenter})`; - }); - if (hiddenLabels) { - this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints'); - } - } - - if (isTooltip) { - squares.call(tooltip.render()); - } - - return squares.selectAll('rect'); - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const squares = self.addSquares(svg, self.chartData); - self.addCircleEvents(squares); - - self.events.emit('rendered', { - chart: self.chartData - }); - - return svg; - }); - }; + const colors = this.handler.visConfig.get('legend.colors', null); + if (colors) { + this.handler.vis.uiState.setSilent('vis.defaultColors', null); + this.handler.vis.uiState.setSilent('vis.defaultColors', colors); } } - return HeatmapChart; + getHeatmapLabels(cfg) { + const percentageMode = cfg.get('percentageMode'); + const colorsNumber = cfg.get('colorsNumber'); + const colorsRange = cfg.get('colorsRange'); + const zAxisConfig = this.getValueAxis().axisConfig; + const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); + const zScale = this.getValueAxis().getScale(); + const [min, max] = zScale.domain(); + const labels = []; + const maxColorCnt = 10; + if (cfg.get('setColorRange')) { + colorsRange.forEach(range => { + const from = isFinite(range.from) ? zAxisFormatter(range.from) : range.from; + const to = isFinite(range.to) ? zAxisFormatter(range.to) : range.to; + labels.push(`${from} - ${to}`); + }); + } else { + if (max === min) { + return [ min.toString() ]; + } + for (let i = 0; i < colorsNumber; i++) { + let label; + let val = i / colorsNumber; + let nextVal = (i + 1) / colorsNumber; + if (percentageMode) { + val = Math.ceil(val * 100); + nextVal = Math.ceil(nextVal * 100); + label = `${val}% - ${nextVal}%`; + } else { + val = val * (max - min) + min; + nextVal = nextVal * (max - min) + min; + if (max - min > maxColorCnt) { + const valInt = Math.ceil(val); + if (i === 0) { + val = (valInt === val ? val : valInt - 1); + } + else{ + val = valInt; + } + nextVal = Math.ceil(nextVal); + } + if (isFinite(val)) val = zAxisFormatter(val); + if (isFinite(nextVal)) nextVal = zAxisFormatter(nextVal); + label = `${val} - ${nextVal}`; + } + + labels.push(label); + } + } + + return labels; + } + + getHeatmapColors(cfg) { + const invertColors = cfg.get('invertColors'); + const colorSchema = cfg.get('colorSchema'); + const labels = this.getHeatmapLabels(cfg); + const colors = {}; + for (const i in labels) { + if (labels[i]) { + const val = invertColors ? 1 - i / labels.length : i / labels.length; + colors[labels[i]] = getHeatmapColors(val, colorSchema); + } + } + return colors; + } + + addSquares(svg, data) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.handler.categoryAxes[1].getScale(); + const zScale = this.getValueAxis().getScale(); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const colorsNumber = this.handler.visConfig.get('colorsNumber'); + const setColorRange = this.handler.visConfig.get('setColorRange'); + const colorsRange = this.handler.visConfig.get('colorsRange'); + const color = this.handler.data.getColorFunc(); + const labels = this.handler.visConfig.get('legend.labels'); + const zAxisConfig = this.getValueAxis().axisConfig; + const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); + const showLabels = zAxisConfig.get('labels.show'); + const overwriteLabelColor = zAxisConfig.get('labels.overwriteColor', false); + + const layer = svg.append('g') + .attr('class', 'series'); + + const squares = layer + .selectAll('g.square') + .data(data.values); + + squares + .exit() + .remove(); + + let barWidth; + if (this.getCategoryAxis().axisConfig.isTimeDomain()) { + const { min, interval } = this.handler.data.get('ordered'); + const start = min; + const end = moment(min).add(interval).valueOf(); + + barWidth = xScale(end) - xScale(start); + if (!isHorizontal) barWidth *= -1; + } + + function x(d) { + return xScale(d.x); + } + + function y(d) { + return yScale(d.series); + } + + const [min, max] = zScale.domain(); + function getColorBucket(d) { + let val = 0; + if (setColorRange && colorsRange.length) { + const bucket = _.find(colorsRange, range => { + return range.from <= d.y && range.to > d.y; + }); + return bucket ? colorsRange.indexOf(bucket) : -1; + } else { + if (isNaN(min) || isNaN(max)) { + val = colorsNumber - 1; + } else if (min === max) { + val = 0; + } else { + val = (d.y - min) / (max - min); /* get val from 0 - 1 */ + val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber)); + } + } + if (d.y == null) { + return -1; + } + return !isNaN(val) ? val : -1; + } + + function label(d) { + const colorBucket = getColorBucket(d); + // colorBucket id should always GTE 0 + if (colorBucket < 0) d.hide = true; + return labels[colorBucket]; + } + + function z(d) { + if (label(d) === '') return 'transparent'; + return color(label(d)); + } + + const squareWidth = barWidth || xScale.rangeBand(); + const squareHeight = yScale.rangeBand(); + + squares + .enter() + .append('g') + .attr('class', 'square'); + + squares.append('rect') + .attr('x', x) + .attr('width', squareWidth) + .attr('y', y) + .attr('height', squareHeight) + .attr('data-label', label) + .attr('fill', z) + .attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px') + .style('display', d => { + return d.hide ? 'none' : 'initial'; + }); + + + // todo: verify that longest label is not longer than the barwidth + // or barwidth is not smaller than textheight (and vice versa) + // + if (showLabels) { + const rotate = zAxisConfig.get('labels.rotate'); + const rotateRad = rotate * Math.PI / 180; + const cellPadding = 5; + const maxLength = Math.min( + Math.abs(squareWidth / Math.cos(rotateRad)), + Math.abs(squareHeight / Math.sin(rotateRad)) + ) - cellPadding; + const maxHeight = Math.min( + Math.abs(squareWidth / Math.sin(rotateRad)), + Math.abs(squareHeight / Math.cos(rotateRad)) + ) - cellPadding; + + let labelColor; + if (overwriteLabelColor) { + // If overwriteLabelColor is true, use the manual specified color + labelColor = zAxisConfig.get('labels.color'); + } else { + // Otherwise provide a function that will calculate a light or dark color + labelColor = d => { + const bgColor = z(d); + const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); + return color && isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])) + ? '#FFF' : '#222'; + }; + } + + let hiddenLabels = false; + squares.append('text') + .text(d => zAxisFormatter(d.y)) + .style('display', function (d) { + const textLength = this.getBBox().width; + const textHeight = this.getBBox().height; + const textTooLong = textLength > maxLength; + const textTooWide = textHeight > maxHeight; + if (!d.hide && (textTooLong || textTooWide)) { + hiddenLabels = true; + } + return d.hide || textTooLong || textTooWide ? 'none' : 'initial'; + }) + .style('dominant-baseline', 'central') + .style('text-anchor', 'middle') + .style('fill', labelColor) + .attr('x', function (d) { + const center = x(d) + squareWidth / 2; + return center; + }) + .attr('y', function (d) { + const center = y(d) + squareHeight / 2; + return center; + }) + .attr('transform', function (d) { + const horizontalCenter = x(d) + squareWidth / 2; + const verticalCenter = y(d) + squareHeight / 2; + return `rotate(${rotate},${horizontalCenter},${verticalCenter})`; + }); + if (hiddenLabels) { + this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints'); + } + } + + if (isTooltip) { + squares.call(tooltip.render()); + } + + return squares.selectAll('rect'); + } + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the line chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + const squares = self.addSquares(svg, self.chartData); + self.addCircleEvents(squares); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + } } diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js b/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js index fc2961f1059c..09111a7ce1d6 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js @@ -19,224 +19,217 @@ import d3 from 'd3'; import _ from 'lodash'; -import { VislibVisualizationsPointSeriesProvider } from './_point_series'; +import { PointSeries } from './_point_series'; -export function VislibVisualizationsLineChartProvider(Private) { - - const PointSeries = Private(VislibVisualizationsPointSeriesProvider); - - const defaults = { - mode: 'normal', - showCircles: true, - radiusRatio: 9, - showLines: true, - interpolate: 'linear', - lineWidth: 2, - color: undefined, - fillColor: undefined - }; - /** - * Line Chart Visualization - * - * @class LineChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ - class LineChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs) { - super(handler, chartEl, chartData, seriesConfigArgs); - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - } - - addCircles(svg, data) { - const self = this; - const showCircles = this.seriesConfig.showCircles; - const color = this.handler.data.getColorFunc(); - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const ordered = this.handler.data.get('ordered'); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const lineWidth = this.seriesConfig.lineWidth; - - const radii = this.baseChart.radii; - - const radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this.seriesConfig.radiusRatio, 2); - - const layer = svg.append('g') - .attr('class', 'points line') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - const circles = layer - .selectAll('circle') - .data(function appendData() { - return data.values.filter(function (d) { - return !_.isNull(d.y) && (d.y || !d.y0); - }); - }); - - circles - .exit() - .remove(); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - const y0 = d.y0 || 0; - const y = d.y || 0; - return yScale(y0 + y); - } - - function cColor(d) { - return color(d.series); - } - - function colorCircle(d) { - const parent = d3.select(this).node().parentNode; - const lengthOfParent = d3.select(parent).data()[0].length; - const isVisible = (lengthOfParent === 1); - - // If only 1 point exists, show circle - if (!showCircles && !isVisible) return 'none'; - return cColor(d); - } - - function getCircleRadiusFn(modifier) { - return function getCircleRadius(d) { - const width = self.baseChart.chartConfig.width; - const height = self.baseChart.chartConfig.height; - const circleRadius = (d.z - radii.min) / radiusStep; - const baseMagicNumber = 2; - - const base = circleRadius ? Math.sqrt(circleRadius + baseMagicNumber) + lineWidth : lineWidth; - return _.min([base, width, height]) + (modifier || 0); - }; - } - - circles - .enter() - .append('circle') - .attr('r', getCircleRadiusFn()) - .attr('fill-opacity', (this.seriesConfig.drawLinesBetweenPoints ? 1 : 0.7)) - .attr('cx', isHorizontal ? cx : cy) - .attr('cy', isHorizontal ? cy : cx) - .attr('class', 'circle-decoration') - .attr('data-label', data.label) - .attr('fill', colorCircle); - - circles - .enter() - .append('circle') - .attr('r', getCircleRadiusFn(10)) - .attr('cx', isHorizontal ? cx : cy) - .attr('cy', isHorizontal ? cy : cx) - .attr('fill', 'transparent') - .attr('class', 'circle') - .attr('data-label', data.label) - .attr('stroke', cColor) - .attr('stroke-width', 0); - - if (isTooltip) { - circles.call(tooltip.render()); - } - - return circles; - } - - /** - * Adds path to SVG - * - * @method addLines - * @param svg {HTMLElement} SVG to which path are appended - * @param data {Array} Array of object data points - * @returns {D3.UpdateSelection} SVG with paths added - */ - addLine(svg, data) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const color = this.handler.data.getColorFunc(); - const ordered = this.handler.data.get('ordered'); - const lineWidth = this.seriesConfig.lineWidth; - const interpolate = this.seriesConfig.interpolate; - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - - const line = svg.append('g') - .attr('class', 'pathgroup lines') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - const y = d.y || 0; - const y0 = d.y0 || 0; - return yScale(y0 + y); - } - - line.append('path') - .attr('data-label', data.label) - .attr('d', () => { - const d3Line = d3.svg.line() - .defined(function (d) { - return !_.isNull(d.y); - }) - .interpolate(interpolate) - .x(isHorizontal ? cx : cy) - .y(isHorizontal ? cy : cx); - return d3Line(data.values); - }) - .attr('fill', 'none') - .attr('stroke', () => { - return color(data.label); - }) - .attr('stroke-width', lineWidth); - - return line; - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - if (self.seriesConfig.drawLinesBetweenPoints) { - self.addLine(svg, self.chartData); - } - const circles = self.addCircles(svg, self.chartData); - self.addCircleEvents(circles); - - self.events.emit('rendered', { - chart: self.chartData - }); - - return svg; - }); - }; - } +const defaults = { + mode: 'normal', + showCircles: true, + radiusRatio: 9, + showLines: true, + interpolate: 'linear', + lineWidth: 2, + color: undefined, + fillColor: undefined +}; +/** + * Line Chart Visualization + * + * @class LineChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ +export class LineChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); } - return LineChart; + addCircles(svg, data) { + const self = this; + const showCircles = this.seriesConfig.showCircles; + const color = this.handler.data.getColorFunc(); + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const ordered = this.handler.data.get('ordered'); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const lineWidth = this.seriesConfig.lineWidth; + + const radii = this.baseChart.radii; + + const radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this.seriesConfig.radiusRatio, 2); + + const layer = svg.append('g') + .attr('class', 'points line') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + const circles = layer + .selectAll('circle') + .data(function appendData() { + return data.values.filter(function (d) { + return !_.isNull(d.y) && (d.y || !d.y0); + }); + }); + + circles + .exit() + .remove(); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function cy(d) { + const y0 = d.y0 || 0; + const y = d.y || 0; + return yScale(y0 + y); + } + + function cColor(d) { + return color(d.series); + } + + function colorCircle(d) { + const parent = d3.select(this).node().parentNode; + const lengthOfParent = d3.select(parent).data()[0].length; + const isVisible = (lengthOfParent === 1); + + // If only 1 point exists, show circle + if (!showCircles && !isVisible) return 'none'; + return cColor(d); + } + + function getCircleRadiusFn(modifier) { + return function getCircleRadius(d) { + const width = self.baseChart.chartConfig.width; + const height = self.baseChart.chartConfig.height; + const circleRadius = (d.z - radii.min) / radiusStep; + const baseMagicNumber = 2; + + const base = circleRadius ? Math.sqrt(circleRadius + baseMagicNumber) + lineWidth : lineWidth; + return _.min([base, width, height]) + (modifier || 0); + }; + } + + circles + .enter() + .append('circle') + .attr('r', getCircleRadiusFn()) + .attr('fill-opacity', (this.seriesConfig.drawLinesBetweenPoints ? 1 : 0.7)) + .attr('cx', isHorizontal ? cx : cy) + .attr('cy', isHorizontal ? cy : cx) + .attr('class', 'circle-decoration') + .attr('data-label', data.label) + .attr('fill', colorCircle); + + circles + .enter() + .append('circle') + .attr('r', getCircleRadiusFn(10)) + .attr('cx', isHorizontal ? cx : cy) + .attr('cy', isHorizontal ? cy : cx) + .attr('fill', 'transparent') + .attr('class', 'circle') + .attr('data-label', data.label) + .attr('stroke', cColor) + .attr('stroke-width', 0); + + if (isTooltip) { + circles.call(tooltip.render()); + } + + return circles; + } + + /** + * Adds path to SVG + * + * @method addLines + * @param svg {HTMLElement} SVG to which path are appended + * @param data {Array} Array of object data points + * @returns {D3.UpdateSelection} SVG with paths added + */ + addLine(svg, data) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const color = this.handler.data.getColorFunc(); + const ordered = this.handler.data.get('ordered'); + const lineWidth = this.seriesConfig.lineWidth; + const interpolate = this.seriesConfig.interpolate; + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + const line = svg.append('g') + .attr('class', 'pathgroup lines') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function cy(d) { + const y = d.y || 0; + const y0 = d.y0 || 0; + return yScale(y0 + y); + } + + line.append('path') + .attr('data-label', data.label) + .attr('d', () => { + const d3Line = d3.svg.line() + .defined(function (d) { + return !_.isNull(d.y); + }) + .interpolate(interpolate) + .x(isHorizontal ? cx : cy) + .y(isHorizontal ? cy : cx); + return d3Line(data.values); + }) + .attr('fill', 'none') + .attr('stroke', () => { + return color(data.label); + }) + .attr('stroke-width', lineWidth); + + return line; + } + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the line chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + if (self.seriesConfig.drawLinesBetweenPoints) { + self.addLine(svg, self.chartData); + } + const circles = self.addCircles(svg, self.chartData); + self.addCircleEvents(circles); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + } } diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/series_types.js b/src/legacy/ui/public/vislib/visualizations/point_series/series_types.js index f7411ed2dd3e..a448c8e72e6d 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/series_types.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/series_types.js @@ -17,17 +17,14 @@ * under the License. */ -import { VislibVisualizationsColumnChartProvider } from './column_chart'; -import { VislibVisualizationsLineChartProvider } from './line_chart'; -import { VislibVisualizationsAreaChartProvider } from './area_chart'; -import { VislibVisualizationsHeatmapChartProvider } from './heatmap_chart'; +import { ColumnChart } from './column_chart'; +import { LineChart } from './line_chart'; +import { AreaChart } from './area_chart'; +import { HeatmapChart } from './heatmap_chart'; -export function VislibVisualizationsSeriesTypesProvider(Private) { - - return { - histogram: Private(VislibVisualizationsColumnChartProvider), - line: Private(VislibVisualizationsLineChartProvider), - area: Private(VislibVisualizationsAreaChartProvider), - heatmap: Private(VislibVisualizationsHeatmapChartProvider) - }; -} +export const seriesTypes = { + histogram: ColumnChart, + line: LineChart, + area: AreaChart, + heatmap: HeatmapChart +}; diff --git a/src/legacy/ui/public/vislib/visualizations/time_marker.js b/src/legacy/ui/public/vislib/visualizations/time_marker.js index 347f62cadcc6..d594969c3439 100644 --- a/src/legacy/ui/public/vislib/visualizations/time_marker.js +++ b/src/legacy/ui/public/vislib/visualizations/time_marker.js @@ -20,72 +20,67 @@ import d3 from 'd3'; import dateMath from '@elastic/datemath'; -export function VislibVisualizationsTimeMarkerProvider() { +export class TimeMarker { + constructor(times, xScale, height) { + const currentTimeArr = [{ + 'time': new Date().getTime(), + 'class': 'time-marker', + 'color': '#c80000', + 'opacity': 0.3, + 'width': 2 + }]; - class TimeMarker { - constructor(times, xScale, height) { - const currentTimeArr = [{ - 'time': new Date().getTime(), - 'class': 'time-marker', - 'color': '#c80000', - 'opacity': 0.3, - 'width': 2 - }]; - - this.xScale = xScale; - this.height = height; - this.times = (times.length) ? times.map(function (d) { - return { - 'time': dateMath.parse(d.time), - 'class': d.class || 'time-marker', - 'color': d.color || '#c80000', - 'opacity': d.opacity || 0.3, - 'width': d.width || 2 - }; - }) : currentTimeArr; - } - - _isTimeBasedChart(selection) { - const data = selection.data(); - return data.every(function (datum) { - return (datum.ordered && datum.ordered.date); - }); - } - - render(selection) { - const self = this; - - // return if not time based chart - if (!self._isTimeBasedChart(selection)) return; - - selection.each(function () { - d3.select(this).selectAll('time-marker') - .data(self.times) - .enter().append('line') - .attr('class', function (d) { - return d.class; - }) - .attr('pointer-events', 'none') - .attr('stroke', function (d) { - return d.color; - }) - .attr('stroke-width', function (d) { - return d.width; - }) - .attr('stroke-opacity', function (d) { - return d.opacity; - }) - .attr('x1', function (d) { - return self.xScale(d.time); - }) - .attr('x2', function (d) { - return self.xScale(d.time); - }) - .attr('y1', self.height) - .attr('y2', self.xScale.range()[0]); - }); - } + this.xScale = xScale; + this.height = height; + this.times = (times.length) ? times.map(function (d) { + return { + 'time': dateMath.parse(d.time), + 'class': d.class || 'time-marker', + 'color': d.color || '#c80000', + 'opacity': d.opacity || 0.3, + 'width': d.width || 2 + }; + }) : currentTimeArr; } - return TimeMarker; + _isTimeBasedChart(selection) { + const data = selection.data(); + return data.every(function (datum) { + return (datum.ordered && datum.ordered.date); + }); + } + + render(selection) { + const self = this; + + // return if not time based chart + if (!self._isTimeBasedChart(selection)) return; + + selection.each(function () { + d3.select(this).selectAll('time-marker') + .data(self.times) + .enter().append('line') + .attr('class', function (d) { + return d.class; + }) + .attr('pointer-events', 'none') + .attr('stroke', function (d) { + return d.color; + }) + .attr('stroke-width', function (d) { + return d.width; + }) + .attr('stroke-opacity', function (d) { + return d.opacity; + }) + .attr('x1', function (d) { + return self.xScale(d.time); + }) + .attr('x2', function (d) { + return self.xScale(d.time); + }) + .attr('y1', self.height) + .attr('y2', self.xScale.range()[0]); + }); + } } diff --git a/src/legacy/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap b/src/legacy/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap index 68a43fa0ca7a..d0539439dd33 100644 --- a/src/legacy/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/legacy/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap @@ -18,24 +18,13 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` > + />
diff --git a/src/legacy/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap b/src/legacy/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap index 1502725f04c3..d494567e6d25 100644 --- a/src/legacy/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap +++ b/src/legacy/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap @@ -12,18 +12,13 @@ exports[`VisualizationRequestError should render according to snapshot 1`] = ` > + />
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index f7e602576f69..e0146b6904ff 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "10.4.1", + "@elastic/eui": "11.0.1", "react": "^16.8.0", "react-dom": "^16.8.0" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index bb8712090bdf..549e2bb48f00 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "10.4.1", + "@elastic/eui": "11.0.1", "react": "^16.8.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 16953de85a0e..8aec51d9c708 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "10.4.1", + "@elastic/eui": "11.0.1", "react": "^16.8.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json index 49321b8fd177..306a768b35a4 100644 --- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json +++ b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "10.4.1", + "@elastic/eui": "11.0.1", "react": "^16.8.0", "react-dom": "^16.8.0" } diff --git a/tsconfig.json b/tsconfig.json index 7d4b6a6e6be8..da4de3b98f76 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,8 +20,9 @@ "strict": true, // enables "core language features" "lib": [ - // ESNext auto includes previous versions all the way back to es5 - "esnext", + // ES2018 includes previous versions all the way back to es5 + // We are not using esnext because @babel/polyfill is on core-js 2, not 3 + "es2018", // includes support for browser APIs "dom" ], diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index f8b94987dbff..b4078bfebfb0 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -41,7 +41,8 @@ export function createJestConfig({ '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, }, transformIgnorePatterns: [ - '[/\\\\]node_modules[/\\\\].+\\.js$' + // ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import() + '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)[/\\\\].+\\.js$' ], snapshotSerializers: [ `${kibanaDirectory}/node_modules/enzyme-to-json/serializer` diff --git a/x-pack/package.json b/x-pack/package.json index 2a420747f1dc..c5e3516a53a1 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -84,7 +84,7 @@ "@types/react-sticky": "^6.0.3", "@types/react-test-renderer": "^16.8.0", "@types/recompose": "^0.30.2", - "@types/reduce-reducers": "^0.1.3", + "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", "@types/rimraf": "^2.0.2", "@types/sinon": "^7.0.0", @@ -120,7 +120,7 @@ "enzyme-to-json": "^3.3.4", "execa": "^1.0.0", "fancy-log": "^1.3.2", - "fetch-mock": "7.3.0", + "fetch-mock": "7.3.3", "graphql-code-generator": "^0.13.0", "graphql-codegen-introspection-template": "^0.13.0", "graphql-codegen-typescript-resolvers-template": "^0.13.0", @@ -168,7 +168,7 @@ "@babel/register": "7.4.4", "@babel/runtime": "7.4.5", "@elastic/datemath": "5.0.2", - "@elastic/eui": "10.4.1", + "@elastic/eui": "11.0.1", "@elastic/javascript-typescript-langserver": "^0.1.27", "@elastic/lsp-extension": "^0.1.1", "@elastic/node-crypto": "^1.0.0", @@ -212,16 +212,16 @@ "copy-to-clipboard": "^3.0.8", "core-js": "2.6.9", "cronstrue": "^1.51.0", - "d3": "3.5.6", - "d3-scale": "1.0.6", + "d3": "3.5.17", + "d3-scale": "1.0.7", "dataloader": "^1.4.0", "dedent": "^0.7.0", - "dragselect": "1.8.1", + "dragselect": "1.12.2", "elasticsearch": "^15.5.0", - "extract-zip": "1.5.0", + "extract-zip": "1.6.7", "file-saver": "^1.3.8", "file-type": "^10.9.0", - "font-awesome": "4.4.0", + "font-awesome": "4.7.0", "formsy-react": "^1.1.5", "get-port": "4.2.0", "getos": "^3.1.0", @@ -246,7 +246,7 @@ "io-ts": "^1.4.2", "joi": "^13.5.2", "jquery": "^3.4.1", - "js-yaml": "3.4.1", + "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.3.0", "lodash": "npm:@elastic/lodash@3.10.1-kibana1", @@ -268,7 +268,7 @@ "moment-duration-format": "^1.3.0", "moment-timezone": "^0.5.14", "memoize-one": "^5.0.0", - "monaco-editor": "^0.14.3", + "monaco-editor": "^0.17.0", "ngreact": "^0.5.1", "nock": "10.0.4", "node-fetch": "^2.1.2", diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 7df0a14e8908..396d985d3784 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -42,26 +42,14 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > + /> @@ -190,18 +178,14 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` - - + /> @@ -266,26 +250,14 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > + /> @@ -374,26 +346,14 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > + /> @@ -522,18 +482,14 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` - - + /> @@ -1008,26 +964,14 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > + /> diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx index 363cfa0ccf65..a68a50c5f6ad 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx @@ -16,13 +16,16 @@ import { asDecimal } from '../../../utils/formatters'; import { Coordinate } from '../../../../typings/timeseries'; +import { getEmptySeries } from '../../shared/charts/CustomPlot/getEmptySeries'; interface Props { + start: number | string | undefined; + end: number | string | undefined; chart: GenericMetricsChart; hoverXHandlers: HoverXHandlers; } -export function MetricsChart({ chart, hoverXHandlers }: Props) { +export function MetricsChart({ start, end, chart, hoverXHandlers }: Props) { const formatYValue = getYTickFormatter(chart); const formatTooltip = getTooltipFormatter(chart); @@ -31,6 +34,8 @@ export function MetricsChart({ chart, hoverXHandlers }: Props) { legendValue: formatYValue(series.overallValue) })); + const noHits = chart.totalHits === 0; + return ( @@ -38,8 +43,8 @@ export function MetricsChart({ chart, hoverXHandlers }: Props) { - series.data.map(coord => coord.y || 0) + ...chart.series.map(({ data }) => + Math.max(...data.map(({ y }) => y || 0)) ) ); return getFixedByteFormatter(max); diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx index 85be3f143f3c..eb7a895cd2b7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx @@ -18,6 +18,7 @@ interface ServiceMetricsProps { export function ServiceMetrics({ urlParams, agentName }: ServiceMetricsProps) { const { data } = useServiceMetricCharts(urlParams, agentName); + const { start, end } = urlParams; return ( ( - + ))} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 1998f4c810e5..ac4327adbc03 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -106,13 +106,12 @@ NodeList [ >
`; @@ -73,26 +61,14 @@ exports[`Storyshots renderers/DropdownFilter with choices 1`] = ` - - - - - + />
`; @@ -131,26 +107,14 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = ` - - - - - + />
`; @@ -189,26 +153,14 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = ` - - - - - + /> `; @@ -229,25 +181,13 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = ` - - - - - + /> `; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot index c325331a620b..05473f55fabe 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot @@ -81,18 +81,14 @@ exports[`Storyshots components/Asset airplane 1`] = ` > + />
@@ -118,18 +114,14 @@ exports[`Storyshots components/Asset airplane 1`] = ` > + /> @@ -156,21 +148,14 @@ exports[`Storyshots components/Asset airplane 1`] = ` > + /> @@ -194,25 +179,14 @@ exports[`Storyshots components/Asset airplane 1`] = ` > + /> @@ -303,18 +277,14 @@ exports[`Storyshots components/Asset marker 1`] = ` > + /> @@ -340,18 +310,14 @@ exports[`Storyshots components/Asset marker 1`] = ` > + /> @@ -378,21 +344,14 @@ exports[`Storyshots components/Asset marker 1`] = ` > + /> @@ -416,25 +375,14 @@ exports[`Storyshots components/Asset marker 1`] = ` > + /> diff --git a/x-pack/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot b/x-pack/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot index 45f32e571f2c..acdcf8689da7 100644 --- a/x-pack/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot @@ -82,23 +82,18 @@ Array [ } > - - + /> ,
- - + />
,
- - + />
,
- - + />
, ] diff --git a/x-pack/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot b/x-pack/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot index a5811a60040c..9e6db06e5a4d 100644 --- a/x-pack/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot @@ -202,19 +202,14 @@ exports[`Storyshots components/ColorManager interactive 1`] = ` > + /> @@ -415,19 +405,14 @@ Array [ > + /> , @@ -512,19 +492,14 @@ Array [ > + /> , @@ -609,19 +579,14 @@ Array [ > + /> , diff --git a/x-pack/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot b/x-pack/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot index ad84c2e54458..14dec33ad073 100644 --- a/x-pack/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot @@ -364,23 +364,18 @@ Array [ } > - - + /> @@ -718,23 +713,18 @@ exports[`Storyshots components/ColorPalette six colors, wrap at 4 1`] = ` } > - - + /> @@ -995,23 +985,18 @@ Array [ } > - - + /> diff --git a/x-pack/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot b/x-pack/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot index ccdb666dd77c..c2f4781d4956 100644 --- a/x-pack/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot @@ -221,19 +221,14 @@ exports[`Storyshots components/ColorPicker interactive 1`] = ` > + /> @@ -317,23 +307,18 @@ exports[`Storyshots components/ColorPicker six colors 1`] = ` } > - - + /> @@ -527,19 +512,14 @@ exports[`Storyshots components/ColorPicker six colors 1`] = ` > + /> @@ -793,19 +768,14 @@ exports[`Storyshots components/ColorPicker six colors, value missing 1`] = ` > + /> @@ -868,23 +833,18 @@ exports[`Storyshots components/ColorPicker three colors 1`] = ` } > - - + /> @@ -1000,19 +960,14 @@ exports[`Storyshots components/ColorPicker three colors 1`] = ` > + /> diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot index 4fac77ebf47e..42eefbb1feb3 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot @@ -56,18 +56,14 @@ Array [ > + />
+ />
@@ -244,22 +236,14 @@ Array [ className="euiCard__top" > - - - + /> + />
+ />
@@ -753,18 +729,14 @@ Array [ > + />
+ />
@@ -941,22 +909,14 @@ Array [ className="euiCard__top" > - - - + /> + />
+ />
@@ -1294,22 +1246,14 @@ Array [ className="euiCard__top" > - - - + /> - - - + /> - - - + /> - - - + /> + />
@@ -71,25 +60,14 @@ exports[`Storyshots components/ElementTypes/ElementControls has two buttons 1`] > + />
diff --git a/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot b/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot index 8748623b2377..cd66f2897544 100644 --- a/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot @@ -71,25 +71,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls 1`] = ` > + />
@@ -112,25 +101,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls 1`] = ` > + />
@@ -196,25 +174,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls 1`] = ` > + />
@@ -237,25 +204,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls 1`] = ` > + />
@@ -321,25 +277,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls 1`] = ` > + />
@@ -362,25 +307,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls 1`] = ` > + />
@@ -461,25 +395,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls and filter > + /> @@ -502,25 +425,14 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls and filter > + /> diff --git a/x-pack/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot b/x-pack/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot index dba0a6bef464..097dad3cf089 100644 --- a/x-pack/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot @@ -21,18 +21,14 @@ exports[`Storyshots components/FileUpload default 1`] = ` > + />
diff --git a/x-pack/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot b/x-pack/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot index c7a5afedda7e..7c433bdb983d 100644 --- a/x-pack/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot @@ -48,26 +48,14 @@ exports[`Storyshots components/FontPicker default 1`] = ` > + />
@@ -142,26 +130,14 @@ exports[`Storyshots components/FontPicker with value 1`] = ` > + /> diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot index 965a7dd9bfb6..79b547102b08 100644 --- a/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot @@ -71,23 +71,18 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` } > - - + />
- - + />
- - + />
@@ -166,47 +151,32 @@ exports[`Storyshots components/ItemGrid icon grid 1`] = ` className="item-grid-row" > - - + /> - - + /> - - + /> `; diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx b/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx index 632e238d1868..94340888eafc 100644 --- a/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx +++ b/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx @@ -16,8 +16,8 @@ storiesOf('components/ItemGrid', module) )) .add('icon grid', () => ( } + items={['plusInCircle', 'minusInCircle', 'check']} + children={item => } /> )) .add('color dot grid', () => ( diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot index 3f8ae898f5cb..939f3fd77753 100644 --- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot @@ -57,18 +57,14 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` > + />
+ />
@@ -92,25 +88,14 @@ exports[`Storyshots components/SidebarHeader/ default 1`] = ` > + /> @@ -167,18 +152,14 @@ exports[`Storyshots components/SidebarHeader/ without layer controls 1`] = ` > + /> @@ -214,25 +195,14 @@ exports[`Storyshots components/SidebarHeader/ without layer controls 1`] = ` > + /> diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot b/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot index c1a8ea30eb60..0d4fa0dcb50d 100644 --- a/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot +++ b/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot @@ -55,31 +55,18 @@ exports[`Storyshots components/Tag as health 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" > - - - - - + />
- - - - - + />
- - - - - + />
- - - - - + />
- - - - - + />
{ (e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS || e.target.type === monaco.editor.MouseTargetType.CONTENT_TEXT) ) { - const lineNumber = (this.props.startLine || 0) + e.target.position.lineNumber; + const position = e.target.position || { lineNumber: 0, column: 0 }; + const lineNumber = (this.props.startLine || 0) + position.lineNumber; this.props.onClick({ lineNumber, - column: e.target.position.column, + column: position.column, }); } }); @@ -94,21 +95,24 @@ export class CodeBlock extends React.PureComponent { prevProps.highlightRanges !== this.props.highlightRanges ) { if (this.ed) { - this.ed.getModel().setValue(this.props.code); + const model = this.ed.getModel(); + if (model) { + model.setValue(this.props.code); - if (this.props.highlightRanges) { - const decorations = this.props.highlightRanges!.map((range: IRange) => { - return { - range, - options: { - inlineClassName: 'codeSearch__highlight', - }, - }; - }); - this.currentHighlightDecorations = this.ed.deltaDecorations( - this.currentHighlightDecorations, - decorations - ); + if (this.props.highlightRanges) { + const decorations = this.props.highlightRanges!.map((range: IRange) => { + return { + range, + options: { + inlineClassName: 'codeSearch__highlight', + }, + }; + }); + this.currentHighlightDecorations = this.ed.deltaDecorations( + this.currentHighlightDecorations, + decorations + ); + } } } } diff --git a/x-pack/plugins/code/public/components/editor/editor.tsx b/x-pack/plugins/code/public/components/editor/editor.tsx index 59195350a0d8..19e2dc244103 100644 --- a/x-pack/plugins/code/public/components/editor/editor.tsx +++ b/x-pack/plugins/code/public/components/editor/editor.tsx @@ -181,7 +181,8 @@ export class EditorComponent extends React.Component { this.editor.onMouseDown((e: editorInterfaces.IEditorMouseEvent) => { if (e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS) { const uri = `${repo}/blob/${encodeRevisionString(revision)}/${file}`; - history.push(`/${uri}!L${e.target.position.lineNumber}:0${qs}`); + const position = e.target.position || { lineNumber: 0, column: 0 }; + history.push(`/${uri}!L${position.lineNumber}:0${qs}`); } this.monaco!.container.focus(); }); diff --git a/x-pack/plugins/code/public/components/file_tree/__snapshots__/file_tree.test.tsx.snap b/x-pack/plugins/code/public/components/file_tree/__snapshots__/file_tree.test.tsx.snap index 63e962517a62..d3f0b3d1c8b5 100644 --- a/x-pack/plugins/code/public/components/file_tree/__snapshots__/file_tree.test.tsx.snap +++ b/x-pack/plugins/code/public/components/file_tree/__snapshots__/file_tree.test.tsx.snap @@ -16,25 +16,14 @@ exports[`render correctly 1`] = ` /> + />
- - - - - + /> - - + /> @@ -148,41 +120,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -210,40 +165,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -273,41 +212,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -335,25 +257,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -385,40 +296,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -451,41 +346,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -513,41 +391,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -579,41 +440,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -641,41 +485,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -703,41 +530,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -765,41 +575,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -827,41 +620,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -889,41 +665,24 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> - - + /> @@ -951,25 +710,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1001,25 +749,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1051,25 +788,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1101,25 +827,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1151,25 +866,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1201,25 +905,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1251,25 +944,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1301,25 +983,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1351,25 +1022,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> @@ -1401,25 +1061,14 @@ exports[`render correctly 1`] = ` tabIndex={0} > - - - - - + /> diff --git a/x-pack/plugins/code/public/components/file_tree/file_tree.test.tsx b/x-pack/plugins/code/public/components/file_tree/file_tree.test.tsx index 99a08f33de3c..cd159fab3e68 100644 --- a/x-pack/plugins/code/public/components/file_tree/file_tree.test.tsx +++ b/x-pack/plugins/code/public/components/file_tree/file_tree.test.tsx @@ -44,7 +44,6 @@ test('render correctly', () => { location={location} closeTreePath={mockFunction} openTreePath={mockFunction} - fetchRepoTree={mockFunction} /> ) .toJSON(); diff --git a/x-pack/plugins/code/public/components/file_tree/file_tree.tsx b/x-pack/plugins/code/public/components/file_tree/file_tree.tsx index 2070407bedfc..061787679846 100644 --- a/x-pack/plugins/code/public/components/file_tree/file_tree.tsx +++ b/x-pack/plugins/code/public/components/file_tree/file_tree.tsx @@ -11,7 +11,7 @@ import classes from 'classnames'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { FileTree as Tree, FileTreeItemType } from '../../../model'; -import { closeTreePath, fetchRepoTree, FetchRepoTreePayload, openTreePath } from '../../actions'; +import { closeTreePath, openTreePath } from '../../actions'; import { EuiSideNavItem, MainRouteParams, PathTypes } from '../../common/types'; import { RootState } from '../../reducers'; import { encodeRevisionString } from '../../utils/url'; @@ -20,9 +20,7 @@ interface Props extends RouteComponentProps { node?: Tree; closeTreePath: (paths: string) => void; openTreePath: (paths: string) => void; - fetchRepoTree: (p: FetchRepoTreePayload) => void; openedPaths: string[]; - treeLoading?: boolean; } export class CodeFileTree extends React.Component { @@ -33,16 +31,6 @@ export class CodeFileTree extends React.Component { } } - public fetchTree(path = '', isDir: boolean) { - const { resource, org, repo, revision } = this.props.match.params; - this.props.fetchRepoTree({ - uri: `${resource}/${org}/${repo}`, - revision, - path: path || '', - isDir, - }); - } - public onClick = (node: Tree) => { const { resource, org, repo, revision, path } = this.props.match.params; if (!(path === node.path)) { @@ -257,11 +245,9 @@ export class CodeFileTree extends React.Component { const mapStateToProps = (state: RootState) => ({ node: state.file.tree, openedPaths: state.file.openedPaths, - treeLoading: state.file.rootFileTreeLoading, }); const mapDispatchToProps = { - fetchRepoTree, closeTreePath, openTreePath, }; diff --git a/x-pack/plugins/code/public/components/main/content.tsx b/x-pack/plugins/code/public/components/main/content.tsx index fae76c4a0916..c8b5aac55af7 100644 --- a/x-pack/plugins/code/public/components/main/content.tsx +++ b/x-pack/plugins/code/public/components/main/content.tsx @@ -53,8 +53,8 @@ interface Props extends RouteComponentProps { onSearchScopeChanged: (s: SearchScope) => void; repoScope: string[]; notFoundDirs: string[]; + fileTreeLoadingPaths: string[]; searchOptions: SearchOptions; - fileTreeLoading: boolean; query: string; } const LANG_MD = 'markdown'; @@ -266,7 +266,7 @@ class CodeContent extends React.PureComponent { } public renderContent() { - const { file, match, tree, fileTreeLoading, isNotFound, notFoundDirs } = this.props; + const { file, match, tree, fileTreeLoadingPaths, isNotFound, notFoundDirs } = this.props; const { path, pathType, resource, org, repo, revision } = match.params; if (isNotFound || notFoundDirs.includes(path || '')) { return ; @@ -281,7 +281,7 @@ class CodeContent extends React.PureComponent { const node = this.findNode(path ? path.split('/') : [], tree); return (
- + ({ notFoundDirs: state.file.notFoundDirs, file: state.file.file, tree: state.file.tree, - fileTreeLoading: state.file.fileTreeLoading, + fileTreeLoadingPaths: state.file.fileTreeLoadingPaths, currentTree: currentTreeSelector(state), branches: state.file.branches, hasMoreCommits: hasMoreCommitsSelector(state), diff --git a/x-pack/plugins/code/public/components/main/main.tsx b/x-pack/plugins/code/public/components/main/main.tsx index 6b9579f880a4..40a51c58ce98 100644 --- a/x-pack/plugins/code/public/components/main/main.tsx +++ b/x-pack/plugins/code/public/components/main/main.tsx @@ -74,7 +74,7 @@ class CodeMain extends React.Component { } const mapStateToProps = (state: RootState) => ({ - loadingFileTree: state.file.rootFileTreeLoading, + loadingFileTree: state.file.fileTreeLoadingPaths.includes(''), loadingStructureTree: state.symbol.loading, hasStructure: structureSelector(state).length > 0 && !state.symbol.error, languageServerInitializing: state.symbol.languageServerInitializing, diff --git a/x-pack/plugins/code/public/components/main/side_tabs.tsx b/x-pack/plugins/code/public/components/main/side_tabs.tsx index dcc4e5e11d0b..16e1a17d22a1 100644 --- a/x-pack/plugins/code/public/components/main/side_tabs.tsx +++ b/x-pack/plugins/code/public/components/main/side_tabs.tsx @@ -71,7 +71,7 @@ class CodeSideTabs extends React.PureComponent { id: Tabs.file, name: 'File', content: fileTabContent, - 'data-test-subj': 'codeFileTreeTab', + 'data-test-subj': `codeFileTreeTab${this.sideTab === Tabs.file ? 'Active' : ''}`, }, { id: Tabs.structure, diff --git a/x-pack/plugins/code/public/components/query_bar/components/__snapshots__/query_bar.test.tsx.snap b/x-pack/plugins/code/public/components/query_bar/components/__snapshots__/query_bar.test.tsx.snap index 481d0717555e..c8e2a2317095 100644 --- a/x-pack/plugins/code/public/components/query_bar/components/__snapshots__/query_bar.test.tsx.snap +++ b/x-pack/plugins/code/public/components/query_bar/components/__snapshots__/query_bar.test.tsx.snap @@ -349,29 +349,21 @@ exports[`render correctly with empty query string 1`] = ` - - - - + /> + Search Everything @@ -408,29 +400,21 @@ exports[`render correctly with empty query string 1`] = ` - - - - + /> + Search Everything @@ -461,40 +445,23 @@ exports[`render correctly with empty query string 1`] = ` className="euiFormControlLayoutCustomIcon__icon" type="arrowDown" > - + /> + @@ -1093,29 +1060,21 @@ exports[`render correctly with input query string changed 1`] = ` - - - - + /> + Search Everything @@ -1152,29 +1111,21 @@ exports[`render correctly with input query string changed 1`] = ` - - - - + /> + Search Everything @@ -1205,40 +1156,23 @@ exports[`render correctly with input query string changed 1`] = ` className="euiFormControlLayoutCustomIcon__icon" type="arrowDown" > - + /> + diff --git a/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap b/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap index daa1c1e2bf9f..87b458f25fa0 100644 --- a/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap +++ b/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap @@ -149,30 +149,21 @@ exports[`render symbol item 1`] = ` - - - - + /> +
diff --git a/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap index 1488a046833e..9d7be6b3ab4c 100644 --- a/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap +++ b/x-pack/plugins/code/public/components/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap @@ -165,29 +165,21 @@ exports[`render full suggestions component 1`] = ` - - - - + /> +
@@ -254,30 +246,21 @@ exports[`render full suggestions component 1`] = ` - - - - + /> +
@@ -338,29 +321,21 @@ exports[`render full suggestions component 1`] = ` - - - - + /> + @@ -464,30 +439,21 @@ exports[`render full suggestions component 1`] = ` - - - - + /> + diff --git a/x-pack/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap b/x-pack/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap index 454a51c2d503..445bbb65adda 100644 --- a/x-pack/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap +++ b/x-pack/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap @@ -19,25 +19,14 @@ exports[`render symbol tree correctly 1`] = ` /> + />
- - - - - + /> - - - + />
- - + />
- - + />
- - + />
- - + />
- - + />
- - + />
- - + />
- - + />
produce(state, draft => { draft.currentPath = action.payload.path; - draft.fileTreeLoading = true; + draft.fileTreeLoadingPaths.push(action.payload!.path); }), [String(fetchRepoTreeSuccess)]: (state: FileState, action: Action) => produce(state, (draft: FileState) => { - draft.fileTreeLoading = false; - draft.rootFileTreeLoading = false; draft.notFoundDirs = draft.notFoundDirs.filter(dir => dir !== action.payload!.path); + draft.fileTreeLoadingPaths = draft.fileTreeLoadingPaths.filter( + p => p !== action.payload!.path && p !== '' + ); const { tree, path, withParents } = action.payload!; if (withParents || path === '/' || path === '') { draft.tree = mergeNode(draft.tree, tree); @@ -142,12 +141,12 @@ export const file = handleActions( }), [String(fetchRootRepoTreeSuccess)]: (state: FileState, action: Action) => produce(state, (draft: FileState) => { - draft.rootFileTreeLoading = false; + draft.fileTreeLoadingPaths = draft.fileTreeLoadingPaths.filter(p => p !== '/' && p !== ''); draft.tree = mergeNode(draft.tree, action.payload!); }), [String(fetchRootRepoTreeFailed)]: (state: FileState, action: Action) => produce(state, (draft: FileState) => { - draft.rootFileTreeLoading = false; + draft.fileTreeLoadingPaths = draft.fileTreeLoadingPaths.filter(p => p !== '/' && p !== ''); }), [String(dirNotFound)]: (state: FileState, action: any) => produce(state, (draft: FileState) => { @@ -158,10 +157,11 @@ export const file = handleActions( draft.tree = initialState.tree; draft.openedPaths = initialState.openedPaths; }), - [String(fetchRepoTreeFailed)]: (state: FileState) => + [String(fetchRepoTreeFailed)]: (state: FileState, action: Action) => produce(state, draft => { - draft.fileTreeLoading = false; - draft.rootFileTreeLoading = false; + draft.fileTreeLoadingPaths = draft.fileTreeLoadingPaths.filter( + p => p !== action.payload!.path && p !== '' + ); }), [String(openTreePath)]: (state: FileState, action: Action) => produce(state, (draft: FileState) => { diff --git a/x-pack/plugins/code/public/reducers/symbol.ts b/x-pack/plugins/code/public/reducers/symbol.ts index b85b7ba36d0f..0dfb6e1f93be 100644 --- a/x-pack/plugins/code/public/reducers/symbol.ts +++ b/x-pack/plugins/code/public/reducers/symbol.ts @@ -73,10 +73,11 @@ const generateStructureTree: (symbols: SymbolInformation[]) => any = symbols => if (result) { return result; } else { - const subTree = tree - .filter(s => s.members) - .map(s => s.members) - .flat(); + // TODO: Use Array.flat once supported + const subTree = tree.reduce( + (s, t) => (t.members ? s.concat(t.members) : s), + [] as SymbolWithMembers[] + ); if (subTree.length > 0) { return findContainer(subTree, containerName); } else { diff --git a/x-pack/plugins/code/public/sagas/editor.ts b/x-pack/plugins/code/public/sagas/editor.ts index 85a0039ecaa3..46eace8f51e2 100644 --- a/x-pack/plugins/code/public/sagas/editor.ts +++ b/x-pack/plugins/code/public/sagas/editor.ts @@ -43,6 +43,7 @@ import { refUrlSelector, repoScopeSelector, urlQueryStringSelector, + createTreeSelector, } from '../selectors'; import { history } from '../utils/url'; import { mainRoutePattern } from './patterns'; @@ -203,15 +204,27 @@ function* handleMainRouteChange(action: Action) { .slice(0, -1) .join('/'); yield put(openTreePath(openPath || '')); - yield put( - fetchRepoTree({ - uri: repoUri, - revision, - path: file || '', - parents: getPathOfTree(tree, (file || '').split('/')) === null, - isDir, - }) - ); + function isTreeLoaded(isDirectory: boolean, targetTree: FileTree | null) { + if (!isDirectory) { + return !!targetTree; + } else if (!targetTree) { + return false; + } else { + return targetTree.children && targetTree.children.length > 0; + } + } + const targetTree: FileTree | null = yield select(createTreeSelector(file || '')); + if (!isTreeLoaded(isDir, targetTree)) { + yield put( + fetchRepoTree({ + uri: repoUri, + revision, + path: file || '', + parents: getPathOfTree(tree, (file || '').split('/')) === null, + isDir, + }) + ); + } const uri = toCanonicalUrl({ repoUri, file, diff --git a/x-pack/plugins/code/public/sagas/file.ts b/x-pack/plugins/code/public/sagas/file.ts index be82da15438c..8a6a9c17f046 100644 --- a/x-pack/plugins/code/public/sagas/file.ts +++ b/x-pack/plugins/code/public/sagas/file.ts @@ -10,7 +10,6 @@ import { kfetch } from 'ui/kfetch'; import Url from 'url'; import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; -import { FileTree } from '../../model'; import { fetchDirectory, fetchDirectoryFailed, @@ -45,51 +44,37 @@ import { dirNotFound, } from '../actions'; import { RootState } from '../reducers'; -import { treeCommitsSelector, createTreeSelector } from '../selectors'; +import { treeCommitsSelector } from '../selectors'; import { repoRoutePattern } from './patterns'; +import { FileTree } from '../../model'; function* handleFetchRepoTree(action: Action) { try { - const { uri, revision, path, parents, isDir } = action.payload!; - if (path && isDir) { - const tree = yield select(createTreeSelector(path)); - if (tree) { - const { children } = tree; - // do not request file tree if this tree exists and its children are not empty - if (!children || children.length === 0) { - yield call(fetchPath, { uri, revision, path, parents, isDir }); - } + const tree = yield call(requestRepoTree, action.payload!); + (tree.children || []).sort((a: FileTree, b: FileTree) => { + const typeDiff = a.type - b.type; + if (typeDiff === 0) { + return a.name > b.name ? 1 : -1; } else { - yield call(fetchPath, { uri, revision, path, parents, isDir }); + return -typeDiff; } - } else { - yield call(fetchPath, action.payload!); - } + }); + tree.repoUri = action.payload!.uri; + yield put( + fetchRepoTreeSuccess({ + tree, + path: action.payload!.path, + withParents: action.payload!.parents, + }) + ); } catch (err) { if (action.payload!.isDir && err.body && err.body.statusCode === 404) { yield put(dirNotFound(action.payload!.path)); } - yield put(fetchRepoTreeFailed(err)); + yield put(fetchRepoTreeFailed({ ...err, path: action.payload!.path })); } } -function* fetchPath(payload: FetchRepoTreePayload) { - const update: FileTree = yield call(requestRepoTree, payload); - (update.children || []).sort((a, b) => { - const typeDiff = a.type - b.type; - if (typeDiff === 0) { - return a.name > b.name ? 1 : -1; - } else { - return -typeDiff; - } - }); - update.repoUri = payload.uri; - yield put( - fetchRepoTreeSuccess({ tree: update, path: payload.path, withParents: payload.parents }) - ); - return update; -} - interface FileTreeQuery { parents?: boolean; limit: number; diff --git a/x-pack/plugins/code/public/sagas/project_status.ts b/x-pack/plugins/code/public/sagas/project_status.ts index c01238dc8763..cbd9c10b5e7b 100644 --- a/x-pack/plugins/code/public/sagas/project_status.ts +++ b/x-pack/plugins/code/public/sagas/project_status.ts @@ -134,7 +134,9 @@ function* handleReposStatusLoaded(action: Action) { // Load current repository status on main page const currentUri = yield select(repoUriSelector); const status = allStatuses[currentUri]; - yield triggerPollRepoStatus(status.state, currentUri); + if (status) { + yield triggerPollRepoStatus(status.state, currentUri); + } } } diff --git a/x-pack/plugins/code/server/__tests__/clone_worker.ts b/x-pack/plugins/code/server/__tests__/clone_worker.ts index a7ee674cd3ff..cd0fc1d81f2f 100644 --- a/x-pack/plugins/code/server/__tests__/clone_worker.ts +++ b/x-pack/plugins/code/server/__tests__/clone_worker.ts @@ -15,8 +15,8 @@ import sinon from 'sinon'; import { Repository } from '../../model'; import { EsClient, Esqueue } from '../lib/esqueue'; import { Logger } from '../log'; -import { CloneWorker } from '../queue'; -import { IndexWorker } from '../queue'; +import { CloneWorker, IndexWorker } from '../queue'; +import { CancellationSerivce } from '../queue/cancellation_service'; import { RepositoryServiceFactory } from '../repository_service_factory'; import { createTestServerOption, emptyAsyncFunc } from '../test_utils'; import { ConsoleLoggerFactory } from '../utils/console_logger_factory'; @@ -93,13 +93,24 @@ describe('clone_worker_tests', () => { const newInstanceSpy = sinon.fake.returns(repoService); repoServiceFactory.newInstance = newInstanceSpy; + // Setup CancellationService + const cancelCloneJobSpy = sinon.spy(); + const registerCloneJobTokenSpy = sinon.spy(); + const cancellationService: any = { + cancelCloneJob: emptyAsyncFunc, + registerCloneJobToken: emptyAsyncFunc, + }; + cancellationService.cancelCloneJob = cancelCloneJobSpy; + cancellationService.registerCloneJobToken = registerCloneJobTokenSpy; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, {} as EsClient, serverOptions, {} as IndexWorker, - (repoServiceFactory as any) as RepositoryServiceFactory + (repoServiceFactory as any) as RepositoryServiceFactory, + cancellationService as CancellationSerivce ); await cloneWorker.executeJob({ @@ -129,13 +140,24 @@ describe('clone_worker_tests', () => { }; esClient.update = updateSpy; + // Setup CancellationService + const cancelCloneJobSpy = sinon.spy(); + const registerCloneJobTokenSpy = sinon.spy(); + const cancellationService: any = { + cancelCloneJob: emptyAsyncFunc, + registerCloneJobToken: emptyAsyncFunc, + }; + cancellationService.cancelCloneJob = cancelCloneJobSpy; + cancellationService.registerCloneJobToken = registerCloneJobTokenSpy; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, esClient as EsClient, serverOptions, (indexWorker as any) as IndexWorker, - {} as RepositoryServiceFactory + {} as RepositoryServiceFactory, + cancellationService as CancellationSerivce ); await cloneWorker.onJobCompleted( @@ -173,13 +195,24 @@ describe('clone_worker_tests', () => { }; esClient.index = indexSpy; + // Setup CancellationService + const cancelCloneJobSpy = sinon.spy(); + const registerCloneJobTokenSpy = sinon.spy(); + const cancellationService: any = { + cancelCloneJob: emptyAsyncFunc, + registerCloneJobToken: emptyAsyncFunc, + }; + cancellationService.cancelCloneJob = cancelCloneJobSpy; + cancellationService.registerCloneJobToken = registerCloneJobTokenSpy; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, (esClient as any) as EsClient, serverOptions, {} as IndexWorker, - {} as RepositoryServiceFactory + {} as RepositoryServiceFactory, + cancellationService as CancellationSerivce ); await cloneWorker.onJobEnqueued({ @@ -209,13 +242,24 @@ describe('clone_worker_tests', () => { const newInstanceSpy = sinon.fake.returns(repoService); repoServiceFactory.newInstance = newInstanceSpy; + // Setup CancellationService + const cancelCloneJobSpy = sinon.spy(); + const registerCloneJobTokenSpy = sinon.spy(); + const cancellationService: any = { + cancelCloneJob: emptyAsyncFunc, + registerCloneJobToken: emptyAsyncFunc, + }; + cancellationService.cancelCloneJob = cancelCloneJobSpy; + cancellationService.registerCloneJobToken = registerCloneJobTokenSpy; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, {} as EsClient, serverOptions, {} as IndexWorker, - (repoServiceFactory as any) as RepositoryServiceFactory + (repoServiceFactory as any) as RepositoryServiceFactory, + cancellationService as CancellationSerivce ); const result1 = await cloneWorker.executeJob({ diff --git a/x-pack/plugins/code/server/__tests__/repository_service.ts b/x-pack/plugins/code/server/__tests__/repository_service.ts index 7eaec9af646d..beae4a306dcf 100644 --- a/x-pack/plugins/code/server/__tests__/repository_service.ts +++ b/x-pack/plugins/code/server/__tests__/repository_service.ts @@ -17,7 +17,6 @@ import { ConsoleLogger } from '../utils/console_logger'; describe('repository service test', () => { const log = new ConsoleLogger(); const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code_test')); - log.debug(baseDir); const repoDir = path.join(baseDir, 'repo'); const credsDir = path.join(baseDir, 'credentials'); // @ts-ignore diff --git a/x-pack/plugins/code/server/init.ts b/x-pack/plugins/code/server/init.ts index 376a259987b5..62d05da535e4 100644 --- a/x-pack/plugins/code/server/init.ts +++ b/x-pack/plugins/code/server/init.ts @@ -181,6 +181,7 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L repoConfigController ); server.events.on('stop', async () => { + log.debug('shutdown lsp process'); await lspService.shutdown(); }); // Initialize indexing factories. @@ -216,7 +217,8 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L esClient, serverOptions, indexWorker, - repoServiceFactory + repoServiceFactory, + cancellationService ).bind(); const deleteWorker = new DeleteWorker( queue, @@ -232,7 +234,8 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L log, esClient, serverOptions, - repoServiceFactory + repoServiceFactory, + cancellationService ).bind(); // Initialize schedulers. diff --git a/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts b/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts index f3b465a00298..73ba48c7119f 100644 --- a/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts +++ b/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts @@ -58,6 +58,16 @@ class MockLauncher extends AbstractLauncher { childProcess.send('listen'); return childProcess; } + + protected killProcess(child: ChildProcess, log: Logger): Promise { + // don't kill the process so fast, otherwise no normal exit can happen + return new Promise(resolve => { + setTimeout(async () => { + const killed = await super.killProcess(child, log); + resolve(killed); + }, 100); + }); + } } class PassiveMockLauncher extends MockLauncher { diff --git a/x-pack/plugins/code/server/lsp/abstract_launcher.ts b/x-pack/plugins/code/server/lsp/abstract_launcher.ts index 5a946f812622..54053467e1dd 100644 --- a/x-pack/plugins/code/server/lsp/abstract_launcher.ts +++ b/x-pack/plugins/code/server/lsp/abstract_launcher.ts @@ -52,9 +52,11 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { this._currentPid = child.pid; this._startTime = Date.now(); this.running = true; - this.onProcessExit(child, () => this.reconnect(proxy, installationPath, port, log)); + this.onProcessExit(child, () => { + if (!proxy.isClosed) this.reconnect(proxy, installationPath, port, log); + }); proxy.onDisconnected(async () => { - this._proxyConnected = true; + this._proxyConnected = false; if (!proxy.isClosed) { log.debug('proxy disconnected, reconnecting'); setTimeout(async () => { @@ -70,12 +72,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { log.debug('proxy exited, is the process running? ' + this.running); if (this.child && this.running) { const p = this.child!; - setTimeout(async () => { - if (!p.killed) { - log.debug('killing the process after 1s'); - await this.killProcess(p, log); - } - }, 1000); + this.killProcess(p, log); } }); proxy.listen(); @@ -164,7 +161,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { log: Logger ): Promise; - private killProcess(child: ChildProcess, log: Logger) { + protected killProcess(child: ChildProcess, log: Logger) { if (!child.killed) { return new Promise((resolve, reject) => { // if not killed within 1s diff --git a/x-pack/plugins/code/server/lsp/proxy.ts b/x-pack/plugins/code/server/lsp/proxy.ts index 7b26bc350f50..23703cd4df8a 100644 --- a/x-pack/plugins/code/server/lsp/proxy.ts +++ b/x-pack/plugins/code/server/lsp/proxy.ts @@ -159,19 +159,17 @@ export class LanguageServerProxy implements ILanguageServerHandler { * https://microsoft.github.io/language-server-protocol/specification#exit */ public async exit() { + this.closed = true; // stop the socket reconnect if (this.clientConnection) { this.logger.info('sending `shutdown` request to language server.'); const clientConn = this.clientConnection; - await clientConn.sendRequest('shutdown').then(() => { - this.logger.info('sending `exit` notification to language server.'); - - // @ts-ignore - // TODO fix this - clientConn.sendNotification(ExitNotification.type); - this.conn.dispose(); // stop listening + clientConn.sendRequest('shutdown').then(() => { + this.conn.dispose(); }); + this.logger.info('sending `exit` notification to language server.'); + // @ts-ignore + clientConn.sendNotification(ExitNotification.type); } - this.closed = true; // stop the socket reconnect this.eventEmitter.emit('exit'); } @@ -263,9 +261,6 @@ export class LanguageServerProxy implements ILanguageServerHandler { } private onSocketClosed() { - if (this.clientConnection) { - this.clientConnection.dispose(); - } this.clientConnection = null; this.connectingPromise = null; this.eventEmitter.emit('close'); diff --git a/x-pack/plugins/code/server/lsp/request_expander.ts b/x-pack/plugins/code/server/lsp/request_expander.ts index df32c2813d8c..337d7896b863 100644 --- a/x-pack/plugins/code/server/lsp/request_expander.ts +++ b/x-pack/plugins/code/server/lsp/request_expander.ts @@ -41,8 +41,9 @@ export class RequestExpander implements ILanguageServerHandler { private jobQueue: Job[] = []; // a map for workspacePath -> Workspace private workspaces: Map = new Map(); - private workspaceRoot: string; + private readonly workspaceRoot: string; private running = false; + private exited = false; constructor( proxy: LanguageServerProxy, @@ -62,6 +63,9 @@ export class RequestExpander implements ILanguageServerHandler { public handleRequest(request: LspRequest): Promise { this.lastAccess = Date.now(); return new Promise((resolve, reject) => { + if (this.exited) { + reject(new Error('proxy is exited.')); + } this.jobQueue.push({ request, resolve, @@ -76,6 +80,7 @@ export class RequestExpander implements ILanguageServerHandler { } public async exit() { + this.exited = true; return this.proxy.exit(); } @@ -152,7 +157,7 @@ export class RequestExpander implements ILanguageServerHandler { private handle() { const job = this.jobQueue.shift(); - if (job) { + if (job && !this.exited) { const { request, resolve, reject } = job; this.expand(request, job.startTime).then( value => { diff --git a/x-pack/plugins/code/server/lsp/workspace_handler.ts b/x-pack/plugins/code/server/lsp/workspace_handler.ts index 45b37210eeff..03556a4ace4f 100644 --- a/x-pack/plugins/code/server/lsp/workspace_handler.ts +++ b/x-pack/plugins/code/server/lsp/workspace_handler.ts @@ -116,10 +116,19 @@ export class WorkspaceHandler { public async listWorkspaceFolders(repoUri: string) { const workspaceDir = await this.workspaceDir(repoUri); const isDir = (source: string) => fs.lstatSync(source).isDirectory(); - return fs - .readdirSync(workspaceDir) - .map(name => path.join(workspaceDir, name)) - .filter(isDir); + try { + return fs + .readdirSync(workspaceDir) + .map(name => path.join(workspaceDir, name)) + .filter(isDir); + } catch (error) { + if (error.code === 'ENOENT') { + this.log.debug('Cannot find workspace dirs'); + return []; + } else { + throw error; + } + } } public async clearWorkspace(repoUri: string) { diff --git a/x-pack/plugins/code/server/queue/abstract_git_worker.ts b/x-pack/plugins/code/server/queue/abstract_git_worker.ts index d644952dcbf8..86f5cd0b048f 100644 --- a/x-pack/plugins/code/server/queue/abstract_git_worker.ts +++ b/x-pack/plugins/code/server/queue/abstract_git_worker.ts @@ -34,6 +34,10 @@ export abstract class AbstractGitWorker extends AbstractWorker { } public async onJobCompleted(job: Job, res: CloneWorkerResult) { + if (res.cancelled) { + // Skip updating job progress if the job is done because of cancellation. + return; + } await super.onJobCompleted(job, res); // Update the default branch. @@ -85,9 +89,9 @@ export abstract class AbstractGitWorker extends AbstractWorker { try { return await this.objectClient.updateRepositoryGitStatus(uri, p); } catch (err) { - // This is a warning since it's not blocking anything. - this.log.warn(`Update git clone progress error.`); - this.log.warn(err); + // Do nothing here since it's not blocking anything. + // this.log.warn(`Update git clone progress error.`); + // this.log.warn(err); } } } diff --git a/x-pack/plugins/code/server/queue/abstract_worker.ts b/x-pack/plugins/code/server/queue/abstract_worker.ts index 8bb16b8d36e0..b1b1d6b243f1 100644 --- a/x-pack/plugins/code/server/queue/abstract_worker.ts +++ b/x-pack/plugins/code/server/queue/abstract_worker.ts @@ -106,12 +106,16 @@ export abstract class AbstractWorker implements Worker { return await this.updateProgress(job, WorkerReservedProgress.INIT); } - public async onJobCompleted(job: Job, res: any) { + public async onJobCompleted(job: Job, res: WorkerResult) { this.log.info( `${this.id} job completed with result ${JSON.stringify( res )} in ${this.workerTaskDurationSeconds(job)} seconds.` ); + if (res.cancelled) { + // Skip updating job progress if the job is done because of cancellation. + return; + } return await this.updateProgress(job, WorkerReservedProgress.COMPLETED); } diff --git a/x-pack/plugins/code/server/queue/cancellation_service.ts b/x-pack/plugins/code/server/queue/cancellation_service.ts index 4cee75e8cd28..c9f9fc454a76 100644 --- a/x-pack/plugins/code/server/queue/cancellation_service.ts +++ b/x-pack/plugins/code/server/queue/cancellation_service.ts @@ -8,13 +8,32 @@ import { RepositoryUri } from '../../model'; import { CancellationToken } from '../lib/esqueue'; export class CancellationSerivce { - // TODO: Add clone/update cancellation map. + private cloneCancellationMap: Map; + private updateCancellationMap: Map; private indexCancellationMap: Map; constructor() { + this.cloneCancellationMap = new Map(); + this.updateCancellationMap = new Map(); this.indexCancellationMap = new Map(); } + public cancelCloneJob(repoUri: RepositoryUri) { + const token = this.cloneCancellationMap.get(repoUri); + if (token) { + token.cancel(); + this.cloneCancellationMap.delete(repoUri); + } + } + + public cancelUpdateJob(repoUri: RepositoryUri) { + const token = this.updateCancellationMap.get(repoUri); + if (token) { + token.cancel(); + this.updateCancellationMap.delete(repoUri); + } + } + public cancelIndexJob(repoUri: RepositoryUri) { const token = this.indexCancellationMap.get(repoUri); if (token) { @@ -23,6 +42,22 @@ export class CancellationSerivce { } } + public registerCloneJobToken(repoUri: RepositoryUri, cancellationToken: CancellationToken) { + const token = this.cloneCancellationMap.get(repoUri); + if (token) { + token.cancel(); + } + this.cloneCancellationMap.set(repoUri, cancellationToken); + } + + public registerUpdateJobToken(repoUri: RepositoryUri, cancellationToken: CancellationToken) { + const token = this.updateCancellationMap.get(repoUri); + if (token) { + token.cancel(); + } + this.updateCancellationMap.set(repoUri, cancellationToken); + } + public registerIndexJobToken(repoUri: RepositoryUri, cancellationToken: CancellationToken) { const token = this.indexCancellationMap.get(repoUri); if (token) { diff --git a/x-pack/plugins/code/server/queue/clone_worker.ts b/x-pack/plugins/code/server/queue/clone_worker.ts index 4786c885fb3a..efbf2847ee8a 100644 --- a/x-pack/plugins/code/server/queue/clone_worker.ts +++ b/x-pack/plugins/code/server/queue/clone_worker.ts @@ -19,6 +19,7 @@ import { Logger } from '../log'; import { RepositoryServiceFactory } from '../repository_service_factory'; import { ServerOptions } from '../server_options'; import { AbstractGitWorker } from './abstract_git_worker'; +import { CancellationSerivce } from './cancellation_service'; import { IndexWorker } from './index_worker'; import { Job } from './job'; @@ -31,13 +32,15 @@ export class CloneWorker extends AbstractGitWorker { protected readonly client: EsClient, protected readonly serverOptions: ServerOptions, private readonly indexWorker: IndexWorker, - private readonly repoServiceFactory: RepositoryServiceFactory + private readonly repoServiceFactory: RepositoryServiceFactory, + private readonly cancellationService: CancellationSerivce ) { super(queue, log, client, serverOptions); } public async executeJob(job: Job) { - const { url } = job.payload; + const { payload, cancellationToken } = job; + const { url } = payload; try { validateGitUrl( url, @@ -62,15 +65,36 @@ export class CloneWorker extends AbstractGitWorker { this.serverOptions.security.enableGitCertCheck ); const repo = RepositoryUtils.buildRepository(url); + + // Try to cancel any existing clone job for this repository. + this.cancellationService.cancelCloneJob(repo.uri); + + let cancelled = false; + if (cancellationToken) { + cancellationToken.on(() => { + cancelled = true; + }); + this.cancellationService.registerCloneJobToken(repo.uri, cancellationToken); + } + return await repoService.clone(repo, (progress: number, cloneProgress?: CloneProgress) => { + if (cancelled) { + // return false to stop the clone progress + return false; + } // For clone job payload, it only has the url. Populate back the // repository uri before update progress. job.payload.uri = repo.uri; this.updateProgress(job, progress, undefined, cloneProgress); + return true; }); } public async onJobCompleted(job: Job, res: CloneWorkerResult) { + if (res.cancelled) { + // Skip updating job progress if the job is done because of cancellation. + return; + } this.log.info(`Clone job done for ${res.repo.uri}`); // For clone job payload, it only has the url. Populate back the // repository uri. diff --git a/x-pack/plugins/code/server/queue/delete_worker.test.ts b/x-pack/plugins/code/server/queue/delete_worker.test.ts index fc8285cb46fb..9f5f02bb49b0 100644 --- a/x-pack/plugins/code/server/queue/delete_worker.test.ts +++ b/x-pack/plugins/code/server/queue/delete_worker.test.ts @@ -41,10 +41,16 @@ test('Execute delete job.', async () => { // Setup CancellationService const cancelIndexJobSpy = sinon.spy(); + const cancelCloneJobSpy = sinon.spy(); + const cancelUpdateJobSpy = sinon.spy(); const cancellationService = { + cancelCloneJob: emptyAsyncFunc, + cancelUpdateJob: emptyAsyncFunc, cancelIndexJob: emptyAsyncFunc, }; cancellationService.cancelIndexJob = cancelIndexJobSpy; + cancellationService.cancelCloneJob = cancelCloneJobSpy; + cancellationService.cancelUpdateJob = cancelUpdateJobSpy; // Setup EsClient const deleteSpy = sinon.fake.returns(Promise.resolve()); @@ -85,6 +91,8 @@ test('Execute delete job.', async () => { }); expect(cancelIndexJobSpy.calledOnce).toBeTruthy(); + expect(cancelCloneJobSpy.calledOnce).toBeTruthy(); + expect(cancelUpdateJobSpy.calledOnce).toBeTruthy(); expect(newInstanceSpy.calledOnce).toBeTruthy(); expect(removeSpy.calledOnce).toBeTruthy(); diff --git a/x-pack/plugins/code/server/queue/delete_worker.ts b/x-pack/plugins/code/server/queue/delete_worker.ts index 0b306f1ecc83..1e7438eac9d4 100644 --- a/x-pack/plugins/code/server/queue/delete_worker.ts +++ b/x-pack/plugins/code/server/queue/delete_worker.ts @@ -40,7 +40,8 @@ export class DeleteWorker extends AbstractWorker { const { uri } = job.payload; // 1. Cancel running workers - // TODO: Add support for clone/update worker. + this.cancellationService.cancelCloneJob(uri); + this.cancellationService.cancelUpdateJob(uri); this.cancellationService.cancelIndexJob(uri); // 2. Delete repository on local fs. diff --git a/x-pack/plugins/code/server/queue/index_worker.ts b/x-pack/plugins/code/server/queue/index_worker.ts index 37c049eb1b46..a7ed181a82a4 100644 --- a/x-pack/plugins/code/server/queue/index_worker.ts +++ b/x-pack/plugins/code/server/queue/index_worker.ts @@ -84,6 +84,7 @@ export class IndexWorker extends AbstractWorker { } // Binding the index cancellation logic + let cancelled = false; this.cancellationService.cancelIndexJob(uri); const indexPromises: Array> = this.indexerFactories.map( async (indexerFactory: IndexerFactory, index: number) => { @@ -96,6 +97,7 @@ export class IndexWorker extends AbstractWorker { if (cancellationToken) { cancellationToken.on(() => { indexer.cancel(); + cancelled = true; }); this.cancellationService.registerIndexJobToken(uri, cancellationToken); } @@ -108,6 +110,7 @@ export class IndexWorker extends AbstractWorker { uri, revision, stats: aggregateIndexStats(stats), + cancelled, }; this.log.info(`Index worker finished with stats: ${JSON.stringify([...res.stats])}`); return res; diff --git a/x-pack/plugins/code/server/queue/update_worker.test.ts b/x-pack/plugins/code/server/queue/update_worker.test.ts index 9eceef62a287..15f657b86c52 100644 --- a/x-pack/plugins/code/server/queue/update_worker.test.ts +++ b/x-pack/plugins/code/server/queue/update_worker.test.ts @@ -5,12 +5,14 @@ */ import sinon from 'sinon'; + import { EsClient, Esqueue } from '../lib/esqueue'; import { Logger } from '../log'; import { RepositoryServiceFactory } from '../repository_service_factory'; import { ServerOptions } from '../server_options'; import { emptyAsyncFunc } from '../test_utils'; import { ConsoleLoggerFactory } from '../utils/console_logger_factory'; +import { CancellationSerivce } from './cancellation_service'; import { UpdateWorker } from './update_worker'; const log: Logger = new ConsoleLoggerFactory().getLogger(['test']); @@ -37,6 +39,16 @@ test('Execute update job', async () => { const newInstanceSpy = sinon.fake.returns(repoService); repoServiceFactory.newInstance = newInstanceSpy; + // Setup CancellationService + const cancelUpdateJobSpy = sinon.spy(); + const registerUpdateJobTokenSpy = sinon.spy(); + const cancellationService: any = { + cancelUpdateJob: emptyAsyncFunc, + registerUpdateJobToken: emptyAsyncFunc, + }; + cancellationService.cancelUpdateJob = cancelUpdateJobSpy; + cancellationService.registerUpdateJobToken = registerUpdateJobTokenSpy; + const updateWorker = new UpdateWorker( esQueue as Esqueue, log, @@ -46,7 +58,8 @@ test('Execute update job', async () => { enableGitCertCheck: false, }, } as ServerOptions, - (repoServiceFactory as any) as RepositoryServiceFactory + (repoServiceFactory as any) as RepositoryServiceFactory, + cancellationService as CancellationSerivce ); await updateWorker.executeJob({ diff --git a/x-pack/plugins/code/server/queue/update_worker.ts b/x-pack/plugins/code/server/queue/update_worker.ts index c5be67439989..e9d3432a6fef 100644 --- a/x-pack/plugins/code/server/queue/update_worker.ts +++ b/x-pack/plugins/code/server/queue/update_worker.ts @@ -10,6 +10,7 @@ import { Logger } from '../log'; import { RepositoryServiceFactory } from '../repository_service_factory'; import { ServerOptions } from '../server_options'; import { AbstractGitWorker } from './abstract_git_worker'; +import { CancellationSerivce } from './cancellation_service'; import { Job } from './job'; export class UpdateWorker extends AbstractGitWorker { @@ -20,13 +21,15 @@ export class UpdateWorker extends AbstractGitWorker { protected readonly log: Logger, protected readonly client: EsClient, protected readonly serverOptions: ServerOptions, - protected readonly repoServiceFactory: RepositoryServiceFactory + protected readonly repoServiceFactory: RepositoryServiceFactory, + private readonly cancellationService: CancellationSerivce ) { super(queue, log, client, serverOptions); } public async executeJob(job: Job) { - const repo: Repository = job.payload; + const { payload, cancellationToken } = job; + const repo: Repository = payload; this.log.info(`Execute update job for ${repo.uri}`); const repoService = this.repoServiceFactory.newInstance( this.serverOptions.repoPath, @@ -34,7 +37,25 @@ export class UpdateWorker extends AbstractGitWorker { this.log, this.serverOptions.security.enableGitCertCheck ); - return await repoService.update(repo); + + // Try to cancel any existing update job for this repository. + this.cancellationService.cancelUpdateJob(repo.uri); + + let cancelled = false; + if (cancellationToken) { + cancellationToken.on(() => { + cancelled = true; + }); + this.cancellationService.registerUpdateJobToken(repo.uri, cancellationToken); + } + + return await repoService.update(repo, () => { + if (cancelled) { + // return false to stop the clone progress + return false; + } + return true; + }); } public async onJobCompleted(job: Job, res: CloneWorkerResult) { diff --git a/x-pack/plugins/code/server/repository_service.ts b/x-pack/plugins/code/server/repository_service.ts index 8ac7408c5a45..99aa39dc02d5 100644 --- a/x-pack/plugins/code/server/repository_service.ts +++ b/x-pack/plugins/code/server/repository_service.ts @@ -8,7 +8,9 @@ import Git, { RemoteCallbacks } from '@elastic/nodegit'; import del from 'del'; import fs from 'fs'; import mkdirp from 'mkdirp'; +import moment from 'moment'; import path from 'path'; + import { RepositoryUtils } from '../common/repository_utils'; import { CloneProgress, @@ -19,10 +21,24 @@ import { } from '../model'; import { Logger } from './log'; -export type CloneProgressHandler = (progress: number, cloneProgress?: CloneProgress) => void; +// Return false to stop the clone progress. Return true to keep going; +export type CloneProgressHandler = (progress: number, cloneProgress?: CloneProgress) => boolean; +export type UpdateProgressHandler = () => boolean; +const GIT_FETCH_PROGRESS_CANCEL = -1; +// TODO: Cannot directly access Git.Error.CODE.EUSER (-7). Investigate why. +const NODEGIT_CALLBACK_RETURN_VALUE_ERROR = -7; +const GIT_INDEXER_PROGRESS_CALLBACK_RETURN_VALUE_ERROR_MSG = `indexer progress callback returned ${GIT_FETCH_PROGRESS_CANCEL}`; const SSH_AUTH_ERROR = new Error('Failed to authenticate SSH session'); +function isCancelled(error: any) { + return ( + error && + (error.message.includes(GIT_INDEXER_PROGRESS_CALLBACK_RETURN_VALUE_ERROR_MSG) || + error.errno === NODEGIT_CALLBACK_RETURN_VALUE_ERROR) + ); +} + // This is the service for any kind of repository handling, e.g. clone, update, delete, etc. export class RepositoryService { constructor( @@ -92,18 +108,34 @@ export class RepositoryService { throw error; } } - public async update(repo: Repository): Promise { + public async update( + repo: Repository, + handler?: UpdateProgressHandler + ): Promise { if (repo.protocol === 'ssh') { - return await this.tryWithKeys(key => this.doUpdate(repo.uri, key)); + return await this.tryWithKeys(key => this.doUpdate(repo.uri, key, handler)); } else { - return await this.doUpdate(repo.uri); + return await this.doUpdate(repo.uri, /* key */ undefined, handler); } } - public async doUpdate(uri: string, key?: string): Promise { + public async doUpdate( + uri: string, + key?: string, + handler?: UpdateProgressHandler + ): Promise { const localPath = RepositoryUtils.repositoryLocalPath(this.repoVolPath, uri); try { const repo = await Git.Repository.open(localPath); const cbs: RemoteCallbacks = { + transferProgress: (_: any) => { + if (handler) { + const resumeUpdate = handler(); + if (!resumeUpdate) { + return GIT_FETCH_PROGRESS_CANCEL; + } + } + return 0; + }, credentials: this.credentialFunc(key), }; // Ignore cert check on testing environment. @@ -133,7 +165,19 @@ export class RepositoryService { revision: headCommit.sha(), }; } catch (error) { - if (error.message && error.message.startsWith(SSH_AUTH_ERROR.message)) { + if (isCancelled(error)) { + // Update job was cancelled intentionally. Do not throw this error. + this.log.info(`Update repository job for ${uri} was cancelled.`); + this.log.debug( + `Update repository job cancellation error: ${JSON.stringify(error, null, 2)}` + ); + return { + uri, + branch: '', + revision: '', + cancelled: true, + }; + } else if (error.message && error.message.startsWith(SSH_AUTH_ERROR.message)) { throw SSH_AUTH_ERROR; } else { const msg = `update repository ${uri} error: ${error}`; @@ -177,30 +221,37 @@ export class RepositoryService { keyFile?: string ) { try { + let lastProgressUpdate = moment(); const cbs: RemoteCallbacks = { - transferProgress: { - // Make the progress update less frequent to avoid too many - // concurrently update of git status in elasticsearch. - throttle: 1000, - callback: (stats: any) => { - if (handler) { - const progress = - (100 * (stats.receivedObjects() + stats.indexedObjects())) / - (stats.totalObjects() * 2); - const cloneProgress = { - isCloned: false, - receivedObjects: stats.receivedObjects(), - indexedObjects: stats.indexedObjects(), - totalObjects: stats.totalObjects(), - localObjects: stats.localObjects(), - totalDeltas: stats.totalDeltas(), - indexedDeltas: stats.indexedDeltas(), - receivedBytes: stats.receivedBytes(), - }; - handler(progress, cloneProgress); + transferProgress: (stats: any) => { + // Clone progress update throttling. + const now = moment(); + if (now.diff(lastProgressUpdate) < 1000) { + return 0; + } + lastProgressUpdate = now; + + if (handler) { + const progress = + (100 * (stats.receivedObjects() + stats.indexedObjects())) / + (stats.totalObjects() * 2); + const cloneProgress = { + isCloned: false, + receivedObjects: stats.receivedObjects(), + indexedObjects: stats.indexedObjects(), + totalObjects: stats.totalObjects(), + localObjects: stats.localObjects(), + totalDeltas: stats.totalDeltas(), + indexedDeltas: stats.indexedDeltas(), + receivedBytes: stats.receivedBytes(), + }; + const resumeClone = handler(progress, cloneProgress); + if (!resumeClone) { + return GIT_FETCH_PROGRESS_CANCEL; } - }, - } as any, + } + return 0; + }, credentials: this.credentialFunc(keyFile), }; // Ignore cert check on testing environment. @@ -234,7 +285,18 @@ export class RepositoryService { }, }; } catch (error) { - if (error.message && error.message.startsWith(SSH_AUTH_ERROR.message)) { + if (isCancelled(error)) { + // Clone job was cancelled intentionally. Do not throw this error. + this.log.info(`Clone repository job for ${repo.uri} was cancelled.`); + this.log.debug( + `Clone repository job cancellation error: ${JSON.stringify(error, null, 2)}` + ); + return { + uri: repo.uri, + repo, + cancelled: true, + }; + } else if (error.message && error.message.startsWith(SSH_AUTH_ERROR.message)) { throw SSH_AUTH_ERROR; } else { const msg = `Clone repository from ${repo.url} error.`; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index 4132c02df863..c4bf2e9374dc 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -287,31 +287,23 @@ exports[`ilm summary extension should return extension when index has lifecycle size="m" type="cross" > - + /> + - - - + />
@@ -113,18 +105,13 @@ exports[`policy table should show empty state when there are not any policies 1` > + /> diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap index 4784586c9208..1443151323bb 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on
"`; +exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on
"`; -exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST
"`; +exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST
"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap index 7ce269bab189..ffe8054c912a 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap @@ -201,17 +201,13 @@ exports[`UploadLicense should display a modal when license requires acknowledgem > + />
+ />
+ />
- + /> + @@ -928,31 +908,23 @@ exports[`UploadLicense should display a modal when license requires acknowledgem size="l" type="importAction" > - + /> +
- +
- +
- + /> +
- +
- - - - + /> +
@@ -541,30 +532,21 @@ exports[`UpgradeFailure component passes expected text for not manual upgrade 1` size="xl" type="alert" > - - - - + /> +
@@ -899,30 +881,21 @@ exports[`UpgradeFailure component passes expected text for not new pipeline 1`] size="xl" type="alert" > - - - - + /> +
diff --git a/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js b/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js index 9e09cb9d8353..97abaa3589cf 100644 --- a/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js +++ b/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js @@ -30,6 +30,7 @@ module.directive('mlNavMenu', function () { scope.name === 'datavisualizer' || scope.name === 'filedatavisualizer' || scope.name === 'timeseriesexplorer' || + scope.name === 'access-denied' || scope.name === 'explorer') { scope.showTabs = true; } diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.ts b/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.ts index fa2b1eea71b4..5d8b1c662565 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.ts +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.ts @@ -80,32 +80,36 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) { fields.forEach(field => { // Group by const availableGroupByAggs = pivotGroupByFieldSupport[field.type]; - availableGroupByAggs.forEach(groupByAgg => { - // Aggregation name for the group-by is the plain field name. Illegal characters will be removed. - const aggName = field.name.replace(illegalEsAggNameChars, '').trim(); - // Option name in the dropdown for the group-by is in the form of `sum(fieldname)`. - const dropDownName = `${groupByAgg}(${field.name})`; - const groupByOption: DropDownLabel = { label: dropDownName }; - groupByOptions.push(groupByOption); - groupByOptionsData[dropDownName] = getDefaultGroupByConfig( - aggName, - dropDownName, - field.name, - groupByAgg - ); - }); + if (availableGroupByAggs !== undefined) { + availableGroupByAggs.forEach(groupByAgg => { + // Aggregation name for the group-by is the plain field name. Illegal characters will be removed. + const aggName = field.name.replace(illegalEsAggNameChars, '').trim(); + // Option name in the dropdown for the group-by is in the form of `sum(fieldname)`. + const dropDownName = `${groupByAgg}(${field.name})`; + const groupByOption: DropDownLabel = { label: dropDownName }; + groupByOptions.push(groupByOption); + groupByOptionsData[dropDownName] = getDefaultGroupByConfig( + aggName, + dropDownName, + field.name, + groupByAgg + ); + }); + } // Aggregations const aggOption: DropDownOption = { label: field.name, options: [] }; const availableAggs = pivotAggsFieldSupport[field.type]; - availableAggs.forEach(agg => { - // Aggregation name is formatted like `fieldname.sum`. Illegal characters will be removed. - const aggName = `${field.name.replace(illegalEsAggNameChars, '').trim()}.${agg}`; - // Option name in the dropdown for the aggregation is in the form of `sum(fieldname)`. - const dropDownName = `${agg}(${field.name})`; - aggOption.options.push({ label: dropDownName }); - aggOptionsData[dropDownName] = { agg, field: field.name, aggName, dropDownName }; - }); + if (availableAggs !== undefined) { + availableAggs.forEach(agg => { + // Aggregation name is formatted like `fieldname.sum`. Illegal characters will be removed. + const aggName = `${field.name.replace(illegalEsAggNameChars, '').trim()}.${agg}`; + // Option name in the dropdown for the aggregation is in the form of `sum(fieldname)`. + const dropDownName = `${agg}(${field.name})`; + aggOption.options.push({ label: dropDownName }); + aggOptionsData[dropDownName] = { agg, field: field.name, aggName, dropDownName }; + }); + } aggOptions.push(aggOption); }); diff --git a/x-pack/plugins/ml/public/explorer/explorer.js b/x-pack/plugins/ml/public/explorer/explorer.js index aaf4b77b66d3..375040301a0a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.js +++ b/x-pack/plugins/ml/public/explorer/explorer.js @@ -12,7 +12,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import DragSelect from 'dragselect'; +import DragSelect from 'dragselect/dist/ds.min.js'; import { map } from 'rxjs/operators'; import { diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js index 3398eb2e168e..1f010d01f47b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -128,7 +128,7 @@ describe('ExplorerChart', () => { expect(paths[0].getAttribute('class')).toBe('domain'); expect(paths[1].getAttribute('class')).toBe('domain'); expect(paths[2].getAttribute('class')).toBe('values-line'); - expect(paths[2].getAttribute('d')).toBe('MNaN,159.33024504444444MNaN,9.166257955555556LNaN,169.60736875555557'); + expect(paths[2].getAttribute('d')).toBe('MNaN,159.33024504444444ZMNaN,9.166257955555556LNaN,169.60736875555557'); const dots = wrapper.getDOMNode().querySelector('.values-dots').querySelectorAll('circle'); expect([...dots]).toHaveLength(1); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js index 26b9710fbd4a..536b0b3b7504 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js @@ -81,7 +81,7 @@ export const watch = { start: { script: { lang: 'painless', - source: `LocalDateTime.ofEpochSecond((doc["timestamp"].date.getMillis()-((doc["bucket_span"].value * 1000) + source: `LocalDateTime.ofEpochSecond((doc["timestamp"].value.getMillis()-((doc["bucket_span"].value * 1000) * params.padding)) / 1000, 0, ZoneOffset.UTC).toString()+\":00.000Z\"`, params: { 'padding': 10 @@ -91,7 +91,7 @@ export const watch = { end: { script: { lang: 'painless', - source: `LocalDateTime.ofEpochSecond((doc["timestamp"].date.getMillis()+((doc["bucket_span"].value * 1000) + source: `LocalDateTime.ofEpochSecond((doc["timestamp"].value.getMillis()+((doc["bucket_span"].value * 1000) * params.padding)) / 1000, 0, ZoneOffset.UTC).toString()+\":00.000Z\"`, params: { 'padding': 10 @@ -101,13 +101,13 @@ export const watch = { timestamp_epoch: { script: { lang: 'painless', - source: 'doc["timestamp"].date.getMillis()/1000' + source: 'doc["timestamp"].value.getMillis()/1000' } }, timestamp_iso8601: { script: { lang: 'painless', - source: 'doc["timestamp"].date' + source: 'doc["timestamp"].value' } }, score: { diff --git a/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js b/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js index d4bd8c488b32..bc688c07cab2 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js @@ -16,7 +16,11 @@ import { checkLicenseExpired, checkBasicLicense } from 'plugins/ml/license/check import { getCreateJobBreadcrumbs, getDataVisualizerIndexOrSearchBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { getDataFrameIndexOrSearchBreadcrumbs } from 'plugins/ml/data_frame/breadcrumbs'; import { preConfiguredJobRedirect } from 'plugins/ml/jobs/new_job/wizard/preconfigured_job_redirect'; -import { checkCreateJobsPrivilege, checkFindFileStructurePrivilege } from 'plugins/ml/privilege/check_privilege'; +import { + checkCreateJobsPrivilege, + checkFindFileStructurePrivilege, + checkCreateDataFrameJobsPrivilege +} from 'plugins/ml/privilege/check_privilege'; import { loadIndexPatterns, getIndexPatterns } from 'plugins/ml/util/index_utils'; import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; import template from './index_or_search.html'; @@ -66,7 +70,7 @@ uiRoutes k7Breadcrumbs: getDataFrameIndexOrSearchBreadcrumbs, resolve: { CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, + privileges: checkCreateDataFrameJobsPrivilege, indexPatterns: loadIndexPatterns, nextStepPath: () => '#data_frames/new_job/step/pivot', } diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap index a4ed9a6fa987..b7c4d4cd0381 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap @@ -12,21 +12,13 @@ exports[`NoData should show a default message if reason is unknown 1`] = ` class="euiPanel euiPanel--paddingLarge euiPageContent eui-textCenter euiPageContent--verticalCenter euiPageContent--horizontalCenter" > - - - + />
@@ -84,21 +76,13 @@ exports[`NoData should show text next to the spinner while checking a setting 1` class="euiPanel euiPanel--paddingLarge euiPageContent eui-textCenter euiPageContent--verticalCenter euiPageContent--horizontalCenter" > - - - + />
diff --git a/x-pack/plugins/remote_clusters/public/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index dd897ac82965..52354124e9ec 100644 --- a/x-pack/plugins/remote_clusters/public/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -257,30 +257,21 @@ Array [ class="euiSwitch__track" > - - + /> - - + />